feat: Migrate staff profile features from Data Connect to V2 REST API
- Removed data_connect package from mobile pubspec.yaml. - Added documentation for V2 profile migration status and QA findings. - Implemented new session management with ClientSessionStore and StaffSessionStore. - Created V2SessionService for handling user sessions via the V2 API. - Developed use cases for cancelling late worker assignments and submitting worker reviews. - Added arguments and use cases for payment chart retrieval and profile completion checks. - Implemented repository interfaces and their implementations for staff main and profile features. - Ensured proper error handling and validation in use cases.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository implementation for the main profile page.
|
||||
///
|
||||
/// Uses the V2 API to fetch staff profile, section statuses, and completion.
|
||||
class ProfileRepositoryImpl {
|
||||
/// Creates a [ProfileRepositoryImpl].
|
||||
ProfileRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
final BaseApiService _api;
|
||||
|
||||
/// Fetches the staff profile from the V2 session endpoint.
|
||||
Future<Staff> getStaffProfile() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffSession);
|
||||
final Map<String, dynamic> json =
|
||||
response.data['staff'] as Map<String, dynamic>;
|
||||
return Staff.fromJson(json);
|
||||
}
|
||||
|
||||
/// Fetches the profile section completion statuses.
|
||||
Future<ProfileSectionStatus> getProfileSections() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffProfileSections);
|
||||
final Map<String, dynamic> json =
|
||||
response.data as Map<String, dynamic>;
|
||||
return ProfileSectionStatus.fromJson(json);
|
||||
}
|
||||
|
||||
/// Signs out the current user.
|
||||
Future<void> signOut() async {
|
||||
await _api.post(V2ApiEndpoints.signOut);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,57 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'profile_state.dart';
|
||||
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
|
||||
import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
|
||||
|
||||
/// Cubit for managing the Profile feature state.
|
||||
///
|
||||
/// Handles loading profile data and user sign-out actions.
|
||||
/// Uses the V2 API via [ProfileRepositoryImpl] for all data fetching.
|
||||
/// Loads the staff profile and section completion statuses in a single flow.
|
||||
class ProfileCubit extends Cubit<ProfileState>
|
||||
with BlocErrorHandler<ProfileState> {
|
||||
/// Creates a [ProfileCubit] with the required repository.
|
||||
ProfileCubit(this._repository) : super(const ProfileState());
|
||||
|
||||
/// Creates a [ProfileCubit] with the required use cases.
|
||||
ProfileCubit(
|
||||
this._getProfileUseCase,
|
||||
this._signOutUseCase,
|
||||
this._getPersonalInfoCompletionUseCase,
|
||||
this._getEmergencyContactsCompletionUseCase,
|
||||
this._getExperienceCompletionUseCase,
|
||||
this._getTaxFormsCompletionUseCase,
|
||||
this._getAttireOptionsCompletionUseCase,
|
||||
this._getStaffDocumentsCompletionUseCase,
|
||||
this._getStaffCertificatesCompletionUseCase,
|
||||
) : super(const ProfileState());
|
||||
final GetStaffProfileUseCase _getProfileUseCase;
|
||||
final SignOutStaffUseCase _signOutUseCase;
|
||||
final GetPersonalInfoCompletionUseCase _getPersonalInfoCompletionUseCase;
|
||||
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
|
||||
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
|
||||
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
|
||||
final GetAttireOptionsCompletionUseCase _getAttireOptionsCompletionUseCase;
|
||||
final GetStaffDocumentsCompletionUseCase _getStaffDocumentsCompletionUseCase;
|
||||
final GetStaffCertificatesCompletionUseCase _getStaffCertificatesCompletionUseCase;
|
||||
final ProfileRepositoryImpl _repository;
|
||||
|
||||
/// Loads the staff member's profile.
|
||||
///
|
||||
/// Emits [ProfileStatus.loading] while fetching data,
|
||||
/// then [ProfileStatus.loaded] with the profile data on success,
|
||||
/// or [ProfileStatus.error] if an error occurs.
|
||||
Future<void> loadProfile() async {
|
||||
emit(state.copyWith(status: ProfileStatus.loading));
|
||||
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final Staff profile = await _getProfileUseCase();
|
||||
final Staff profile = await _repository.getStaffProfile();
|
||||
emit(state.copyWith(status: ProfileStatus.loaded, profile: profile));
|
||||
},
|
||||
onError:
|
||||
(String errorKey) =>
|
||||
state.copyWith(status: ProfileStatus.error, errorMessage: errorKey),
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: ProfileStatus.error, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads all profile section completion statuses in a single V2 API call.
|
||||
Future<void> loadSectionStatuses() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final ProfileSectionStatus sections =
|
||||
await _repository.getProfileSections();
|
||||
emit(state.copyWith(
|
||||
personalInfoComplete: sections.personalInfoCompleted,
|
||||
emergencyContactsComplete: sections.emergencyContactCompleted,
|
||||
experienceComplete: sections.experienceCompleted,
|
||||
taxFormsComplete: sections.taxFormsCompleted,
|
||||
attireComplete: sections.attireCompleted,
|
||||
certificatesComplete: sections.certificateCount > 0,
|
||||
));
|
||||
},
|
||||
onError: (String _) => state,
|
||||
);
|
||||
}
|
||||
|
||||
/// Signs out the current user.
|
||||
///
|
||||
/// Delegates to the sign-out use case which handles session cleanup
|
||||
/// and navigation.
|
||||
Future<void> signOut() async {
|
||||
if (state.status == ProfileStatus.loading) {
|
||||
return;
|
||||
@@ -67,116 +62,11 @@ class ProfileCubit extends Cubit<ProfileState>
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _signOutUseCase();
|
||||
await _repository.signOut();
|
||||
emit(state.copyWith(status: ProfileStatus.signedOut));
|
||||
},
|
||||
onError: (String _) {
|
||||
// For sign out errors, we might want to just proceed or show error
|
||||
// Current implementation was silent catch, let's keep it robust but consistent
|
||||
// If we want to force navigation even on error, we would do it here
|
||||
// But usually handleError emits the error state.
|
||||
// Let's stick to standard error reporting for now.
|
||||
return state.copyWith(status: ProfileStatus.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads personal information completion status.
|
||||
Future<void> loadPersonalInfoCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool isComplete = await _getPersonalInfoCompletionUseCase();
|
||||
emit(state.copyWith(personalInfoComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(personalInfoComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads emergency contacts completion status.
|
||||
Future<void> loadEmergencyContactsCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool isComplete = await _getEmergencyContactsCompletionUseCase();
|
||||
emit(state.copyWith(emergencyContactsComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(emergencyContactsComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads experience completion status.
|
||||
Future<void> loadExperienceCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool isComplete = await _getExperienceCompletionUseCase();
|
||||
emit(state.copyWith(experienceComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(experienceComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads tax forms completion status.
|
||||
Future<void> loadTaxFormsCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool isComplete = await _getTaxFormsCompletionUseCase();
|
||||
emit(state.copyWith(taxFormsComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(taxFormsComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads attire options completion status.
|
||||
Future<void> loadAttireCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getAttireOptionsCompletionUseCase();
|
||||
emit(state.copyWith(attireComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(attireComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads documents completion status.
|
||||
Future<void> loadDocumentsCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getStaffDocumentsCompletionUseCase();
|
||||
emit(state.copyWith(documentsComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(documentsComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads certificates completion status.
|
||||
Future<void> loadCertificatesCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getStaffCertificatesCompletionUseCase();
|
||||
emit(state.copyWith(certificatesComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(certificatesComplete: false);
|
||||
},
|
||||
onError: (String _) =>
|
||||
state.copyWith(status: ProfileStatus.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,19 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../blocs/profile_cubit.dart';
|
||||
import '../blocs/profile_state.dart';
|
||||
import '../widgets/logout_button.dart';
|
||||
import '../widgets/header/profile_header.dart';
|
||||
import '../widgets/profile_page_skeleton/profile_page_skeleton.dart';
|
||||
import '../widgets/reliability_score_bar.dart';
|
||||
import '../widgets/reliability_stats_card.dart';
|
||||
import '../widgets/sections/index.dart';
|
||||
import 'package:staff_profile/src/presentation/blocs/profile_cubit.dart';
|
||||
import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/logout_button.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/header/profile_header.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/profile_page_skeleton/profile_page_skeleton.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/reliability_score_bar.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/reliability_stats_card.dart';
|
||||
import 'package:staff_profile/src/presentation/widgets/sections/index.dart';
|
||||
|
||||
/// The main Staff Profile page.
|
||||
///
|
||||
/// This page displays the staff member's profile including their stats,
|
||||
/// reliability score, and various menu sections for onboarding, compliance,
|
||||
/// learning, finance, and support.
|
||||
///
|
||||
/// It follows Clean Architecture with BLoC for state management.
|
||||
/// Displays the staff member's profile, reliability stats, and
|
||||
/// various menu sections. Uses V2 API via [ProfileCubit].
|
||||
class StaffProfilePage extends StatelessWidget {
|
||||
/// Creates a [StaffProfilePage].
|
||||
const StaffProfilePage({super.key});
|
||||
@@ -40,16 +37,10 @@ class StaffProfilePage extends StatelessWidget {
|
||||
value: cubit,
|
||||
child: BlocConsumer<ProfileCubit, ProfileState>(
|
||||
listener: (BuildContext context, ProfileState state) {
|
||||
// Load completion statuses when profile loads successfully
|
||||
// Load section statuses when profile loads successfully
|
||||
if (state.status == ProfileStatus.loaded &&
|
||||
state.personalInfoComplete == null) {
|
||||
cubit.loadPersonalInfoCompletion();
|
||||
cubit.loadEmergencyContactsCompletion();
|
||||
cubit.loadExperienceCompletion();
|
||||
cubit.loadTaxFormsCompletion();
|
||||
cubit.loadAttireCompletion();
|
||||
cubit.loadDocumentsCompletion();
|
||||
cubit.loadCertificatesCompletion();
|
||||
cubit.loadSectionStatuses();
|
||||
}
|
||||
|
||||
if (state.status == ProfileStatus.signedOut) {
|
||||
@@ -64,7 +55,6 @@ class StaffProfilePage extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, ProfileState state) {
|
||||
// Show shimmer skeleton while profile data loads
|
||||
if (state.status == ProfileStatus.loading) {
|
||||
return const ProfilePageSkeleton();
|
||||
}
|
||||
@@ -96,8 +86,8 @@ class StaffProfilePage extends StatelessWidget {
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ProfileHeader(
|
||||
fullName: profile.name,
|
||||
photoUrl: profile.avatar,
|
||||
fullName: profile.fullName,
|
||||
photoUrl: null,
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -UiConstants.space6),
|
||||
@@ -108,33 +98,27 @@ class StaffProfilePage extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: UiConstants.space6,
|
||||
children: <Widget>[
|
||||
// Reliability Stats and Score
|
||||
// Reliability Stats
|
||||
ReliabilityStatsCard(
|
||||
totalShifts: profile.totalShifts,
|
||||
totalShifts: 0,
|
||||
averageRating: profile.averageRating,
|
||||
onTimeRate: profile.onTimeRate,
|
||||
noShowCount: profile.noShowCount,
|
||||
cancellationCount: profile.cancellationCount,
|
||||
onTimeRate: 0,
|
||||
noShowCount: 0,
|
||||
cancellationCount: 0,
|
||||
),
|
||||
|
||||
// Reliability Score Bar
|
||||
ReliabilityScoreBar(
|
||||
reliabilityScore: profile.reliabilityScore,
|
||||
const ReliabilityScoreBar(
|
||||
reliabilityScore: 0,
|
||||
),
|
||||
|
||||
// Ordered sections
|
||||
const OnboardingSection(),
|
||||
|
||||
// Compliance section
|
||||
const ComplianceSection(),
|
||||
|
||||
// Finance section
|
||||
const FinanceSection(),
|
||||
|
||||
// Support section
|
||||
const SupportSection(),
|
||||
|
||||
// Logout button at the bottom
|
||||
// Logout button
|
||||
const LogoutButton(),
|
||||
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
@@ -18,12 +18,12 @@ class ProfileLevelBadge extends StatelessWidget {
|
||||
String _mapStatusToLevel(StaffStatus status) {
|
||||
switch (status) {
|
||||
case StaffStatus.active:
|
||||
case StaffStatus.verified:
|
||||
return 'KROWER I';
|
||||
case StaffStatus.pending:
|
||||
case StaffStatus.completedProfile:
|
||||
case StaffStatus.invited:
|
||||
return 'Pending';
|
||||
default:
|
||||
case StaffStatus.inactive:
|
||||
case StaffStatus.blocked:
|
||||
case StaffStatus.unknown:
|
||||
return 'New';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'presentation/blocs/profile_cubit.dart';
|
||||
import 'presentation/pages/staff_profile_page.dart';
|
||||
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
|
||||
import 'package:staff_profile/src/presentation/blocs/profile_cubit.dart';
|
||||
import 'package:staff_profile/src/presentation/pages/staff_profile_page.dart';
|
||||
|
||||
/// The entry module for the Staff Profile feature.
|
||||
///
|
||||
/// This module provides dependency injection bindings for the profile feature
|
||||
/// following Clean Architecture principles.
|
||||
///
|
||||
/// Dependency flow:
|
||||
/// - Use cases from data_connect layer (StaffConnectorRepository)
|
||||
/// - Cubit depends on use cases
|
||||
/// Uses the V2 REST API via [BaseApiService] for all backend access.
|
||||
/// Section completion statuses are fetched in a single API call.
|
||||
class StaffProfileModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// StaffConnectorRepository intialization
|
||||
i.addLazySingleton<StaffConnectorRepository>(
|
||||
() => StaffConnectorRepositoryImpl(),
|
||||
);
|
||||
|
||||
// Use cases from data_connect - depend on StaffConnectorRepository
|
||||
i.addLazySingleton<GetStaffProfileUseCase>(
|
||||
() =>
|
||||
GetStaffProfileUseCase(repository: i.get<StaffConnectorRepository>()),
|
||||
);
|
||||
i.addLazySingleton<SignOutStaffUseCase>(
|
||||
() => SignOutStaffUseCase(repository: i.get<StaffConnectorRepository>()),
|
||||
);
|
||||
i.addLazySingleton<GetPersonalInfoCompletionUseCase>(
|
||||
() => GetPersonalInfoCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetEmergencyContactsCompletionUseCase>(
|
||||
() => GetEmergencyContactsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetExperienceCompletionUseCase>(
|
||||
() => GetExperienceCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetTaxFormsCompletionUseCase>(
|
||||
() => GetTaxFormsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetAttireOptionsCompletionUseCase>(
|
||||
() => GetAttireOptionsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetStaffDocumentsCompletionUseCase>(
|
||||
() => GetStaffDocumentsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetStaffCertificatesCompletionUseCase>(
|
||||
() => GetStaffCertificatesCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
// Repository
|
||||
i.addLazySingleton<ProfileRepositoryImpl>(
|
||||
() => ProfileRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Presentation layer - Cubit as singleton to avoid recreation
|
||||
// BlocProvider will use this same instance, preventing state emission after close
|
||||
// Cubit
|
||||
i.addLazySingleton<ProfileCubit>(
|
||||
() => ProfileCubit(
|
||||
i.get<GetStaffProfileUseCase>(),
|
||||
i.get<SignOutStaffUseCase>(),
|
||||
i.get<GetPersonalInfoCompletionUseCase>(),
|
||||
i.get<GetEmergencyContactsCompletionUseCase>(),
|
||||
i.get<GetExperienceCompletionUseCase>(),
|
||||
i.get<GetTaxFormsCompletionUseCase>(),
|
||||
i.get<GetAttireOptionsCompletionUseCase>(),
|
||||
i.get<GetStaffDocumentsCompletionUseCase>(),
|
||||
i.get<GetStaffCertificatesCompletionUseCase>(),
|
||||
),
|
||||
() => ProfileCubit(i.get<ProfileRepositoryImpl>()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user