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:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

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

View File

@@ -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),
);
}
}

View File

@@ -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),

View File

@@ -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';
}
}

View File

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