Refactor staff profile management to remove livePhoto references and update repository methods for current user authentication

This commit is contained in:
Achintha Isuru
2026-01-26 18:41:13 -05:00
parent f4ac292c14
commit 060036fe36
14 changed files with 106 additions and 213 deletions

View File

@@ -28,7 +28,6 @@ class ProfileRepositoryMock {
phone: '555-123-4567',
status: StaffStatus.active,
avatar: null,
livePhoto: null,
address: null,
);
}

View File

@@ -166,7 +166,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
status: domain.StaffStatus.completedProfile,
address: staffRecord.addres,
avatar: staffRecord.photoUrl,
livePhoto: null,
);
StaffSessionStore.instance.setSession(
StaffSession(user: domainUser, staff: domainStaff),

View File

@@ -1,3 +1,4 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
@@ -16,16 +17,26 @@ import '../../domain/repositories/profile_repository.dart';
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
/// Creates a [ProfileRepositoryImpl].
///
/// Requires a [ExampleConnector] from the data_connect package.
const ProfileRepositoryImpl({required this.connector});
/// Requires a [ExampleConnector] from the data_connect package and [FirebaseAuth].
const ProfileRepositoryImpl({
required this.connector,
required this.firebaseAuth,
});
/// The Data Connect connector used for data operations.
final ExampleConnector connector;
/// The Firebase Auth instance.
final FirebaseAuth firebaseAuth;
@override
Future<Staff> getStaffProfile(String userId) async {
// ignore: always_specify_types
final response = await connector.getStaffByUserId(userId: userId).execute();
Future<Staff> getStaffProfile() async {
final user = firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
}
final response = await connector.getStaffByUserId(userId: user.uid).execute();
if (response.data.staffs.isEmpty) {
// TODO: Handle user not found properly with domain exception
@@ -43,7 +54,7 @@ class ProfileRepositoryImpl implements ProfileRepositoryInterface {
phone: rawStaff.phone,
avatar: rawStaff.photoUrl,
status: StaffStatus.active,
address: null,
address: rawStaff.addres,
totalShifts: rawStaff.totalShifts,
averageRating: rawStaff.averageRating,
onTimeRate: rawStaff.onTimeRate,
@@ -54,9 +65,11 @@ class ProfileRepositoryImpl implements ProfileRepositoryInterface {
}
@override
Future<void> signOut() {
// TODO: Implement sign out via Auth interface, not profile repository
// For now, no-op or delegate if connector has auth
return Future.value();
Future<void> signOut() async {
try {
await firebaseAuth.signOut();
} catch (e) {
throw Exception('Error signing out: ${e.toString()}');
}
}
}

View File

@@ -10,13 +10,13 @@ import 'package:krow_domain/krow_domain.dart';
/// - Defines business requirements without implementation details
/// - Allows the domain layer to be independent of data sources
abstract interface class ProfileRepositoryInterface {
/// Fetches the staff profile for the given user ID.
/// Fetches the staff profile for the current authenticated user.
///
/// Returns a [Staff] entity from the shared domain package containing
/// all profile information.
///
/// Throws an exception if the profile cannot be retrieved.
Future<Staff> getStaffProfile(String userId);
Future<Staff> getStaffProfile();
/// Signs out the current user.
///

View File

@@ -1,118 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// UI entity representing extended profile information for a staff member.
///
/// This UI entity wraps the shared [Staff] domain entity and adds
/// presentation-layer specific data such as:
/// - Reliability statistics (shifts, ratings, etc.)
/// - Profile completion status
/// - Performance metrics
///
/// Following Clean Architecture principles, this entity:
/// - Lives in the feature's domain/ui_entities layer
/// - Is used only by the presentation layer
/// - Extends the core [Staff] entity with UI-specific data
/// - Does NOT replace domain entities in repositories or use cases
class StaffProfileUI extends Equatable {
/// The underlying staff entity from the shared domain
final Staff staff;
/// Total number of shifts worked
final int totalShifts;
/// Average rating received from clients (0.0 - 5.0)
final double averageRating;
/// Percentage of shifts where staff arrived on time
final int onTimeRate;
/// Number of times the staff failed to show up for a shift
final int noShowCount;
/// Number of shifts the staff has cancelled
final int cancellationCount;
/// Overall reliability score (0-100)
final int reliabilityScore;
/// Whether personal information section is complete
final bool hasPersonalInfo;
/// Whether emergency contact section is complete
final bool hasEmergencyContact;
/// Whether work experience section is complete
final bool hasExperience;
/// Whether attire photo has been uploaded
final bool hasAttire;
/// Whether required documents have been uploaded
final bool hasDocuments;
/// Whether certificates have been uploaded
final bool hasCertificates;
/// Whether tax forms have been submitted
final bool hasTaxForms;
const StaffProfileUI({
required this.staff,
required this.totalShifts,
required this.averageRating,
required this.onTimeRate,
required this.noShowCount,
required this.cancellationCount,
required this.reliabilityScore,
required this.hasPersonalInfo,
required this.hasEmergencyContact,
required this.hasExperience,
required this.hasAttire,
required this.hasDocuments,
required this.hasCertificates,
required this.hasTaxForms,
});
/// Convenience getters that delegate to the underlying Staff entity
String get fullName => staff.name;
String get email => staff.email;
String? get photoUrl => staff.avatar;
String get userId => staff.authProviderId;
String get staffId => staff.id;
/// Maps staff status to a level string for display
/// TODO: Replace with actual level data when available in Staff entity
String get level => _mapStatusToLevel(staff.status);
String _mapStatusToLevel(StaffStatus status) {
switch (status) {
case StaffStatus.active:
case StaffStatus.verified:
return 'Krower I';
case StaffStatus.pending:
case StaffStatus.completedProfile:
return 'Pending';
default:
return 'New';
}
}
@override
List<Object?> get props => [
staff,
totalShifts,
averageRating,
onTimeRate,
noShowCount,
cancellationCount,
reliabilityScore,
hasPersonalInfo,
hasEmergencyContact,
hasExperience,
hasAttire,
hasDocuments,
hasCertificates,
hasTaxForms,
];
}

View File

@@ -2,22 +2,14 @@ import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/profile_repository.dart';
import '../ui_entities/staff_profile_ui.dart';
/// Use case for fetching a staff member's extended profile information.
///
/// This use case:
/// 1. Fetches the core [Staff] entity from the repository
/// 2. Maps it to a [StaffProfileUI] entity with additional UI-specific data
/// 1. Fetches the [Staff] object from the repository
/// 2. Returns it directly to the presentation layer
///
/// Following Clean Architecture principles:
/// - Depends only on the repository interface (dependency inversion)
/// - Returns a UI entity suitable for the presentation layer
/// - Encapsulates the mapping logic from domain to UI entities
///
/// TODO: When profile statistics API is available, fetch and map real data
/// Currently returns mock statistics data.
class GetProfileUseCase implements UseCase<String, StaffProfileUI> {
class GetProfileUseCase implements UseCase<void, Staff> {
final ProfileRepositoryInterface _repository;
/// Creates a [GetProfileUseCase].
@@ -26,26 +18,8 @@ class GetProfileUseCase implements UseCase<String, StaffProfileUI> {
const GetProfileUseCase(this._repository);
@override
Future<StaffProfileUI> call(String userId) async {
// Fetch core Staff entity from repository
final staff = await _repository.getStaffProfile(userId);
// Map to UI entity with additional profile data
return StaffProfileUI(
staff: staff,
totalShifts: staff.totalShifts ?? 0,
averageRating: staff.averageRating ?? 5.0,
onTimeRate: staff.onTimeRate ?? 0,
noShowCount: staff.noShowCount ?? 0,
cancellationCount: staff.cancellationCount ?? 0,
reliabilityScore: staff.reliabilityScore ?? 100,
hasPersonalInfo: staff.phone != null && staff.phone!.isNotEmpty,
hasEmergencyContact: false, // TODO: Fetch from backend
hasExperience: false, // TODO: Fetch from backend
hasAttire: false, // TODO: Check attire items from backend when available
hasDocuments: false, // TODO: Fetch from backend
hasCertificates: false, // TODO: Fetch from backend
hasTaxForms: false, // TODO: Fetch from backend
);
Future<Staff> call([void params]) async {
// Fetch staff object from repository and return directly
return await _repository.getStaffProfile();
}
}

View File

@@ -21,13 +21,11 @@ class ProfileCubit extends Cubit<ProfileState> {
/// Emits [ProfileStatus.loading] while fetching data,
/// then [ProfileStatus.loaded] with the profile data on success,
/// or [ProfileStatus.error] if an error occurs.
///
/// Requires [userId] to identify which profile to load.
Future<void> loadProfile(String userId) async {
Future<void> loadProfile() async {
emit(state.copyWith(status: ProfileStatus.loading));
try {
final profile = await _getProfileUseCase(userId);
final profile = await _getProfileUseCase();
emit(state.copyWith(
status: ProfileStatus.loaded,
profile: profile,
@@ -45,8 +43,15 @@ class ProfileCubit extends Cubit<ProfileState> {
/// Delegates to the sign-out use case which handles session cleanup
/// and navigation.
Future<void> signOut() async {
if (state.status == ProfileStatus.loading) {
return;
}
emit(state.copyWith(status: ProfileStatus.loading));
try {
await _signOutUseCase();
emit(state.copyWith(status: ProfileStatus.signedOut));
} catch (e) {
// Error handling can be added here if needed
// For now, we let the navigation happen regardless

View File

@@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/ui_entities/staff_profile_ui.dart';
import 'package:krow_domain/krow_domain.dart';
/// Represents the various states of the profile feature.
enum ProfileStatus {
@@ -13,6 +12,9 @@ enum ProfileStatus {
/// Profile data loaded successfully
loaded,
/// User successfully signed out
signedOut,
/// An error occurred while loading profile data
error,
}
@@ -20,14 +22,13 @@ enum ProfileStatus {
/// State class for the Profile feature.
///
/// Contains the current profile data and loading status.
/// Uses the [StaffProfileUI] entity which wraps the shared Staff entity
/// with presentation-layer specific data.
/// Uses the [Staff] entity directly from domain layer.
class ProfileState extends Equatable {
/// Current status of the profile feature
final ProfileStatus status;
/// The staff member's profile UI entity (null if not loaded)
final StaffProfileUI? profile;
/// The staff member's profile object (null if not loaded)
final Staff? profile;
/// Error message if status is error
final String? errorMessage;
@@ -41,7 +42,7 @@ class ProfileState extends Equatable {
/// Creates a copy of this state with updated values.
ProfileState copyWith({
ProfileStatus? status,
StaffProfileUI? profile,
Staff? profile,
String? errorMessage,
}) {
return ProfileState(

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart' hide ReadContext;
import 'package:flutter_modular/flutter_modular.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/profile_cubit.dart';
import '../blocs/profile_state.dart';
@@ -26,28 +27,50 @@ class StaffProfilePage extends StatelessWidget {
/// Creates a [StaffProfilePage].
const StaffProfilePage({super.key});
String _mapStatusToLevel(StaffStatus status) {
switch (status) {
case StaffStatus.active:
case StaffStatus.verified:
return 'Krower I';
case StaffStatus.pending:
case StaffStatus.completedProfile:
return 'Pending';
default:
return 'New';
}
}
void _onSignOut(ProfileCubit cubit, ProfileState state) {
if (state.status != ProfileStatus.loading) {
cubit.signOut();
}
}
@override
Widget build(BuildContext context) {
final TranslationsStaffProfileEn i18n = t.staff.profile;
final ProfileCubit cubit = Modular.get<ProfileCubit>();
// Load profile data on first build
// TODO: Get actual userId from auth session
// For now, using mock userId that matches ProfileRepositoryMock data
const userId = 't8P3fYh4y1cPoZbbVPXUhfQCsDo3';
if (cubit.state.status == ProfileStatus.initial) {
cubit.loadProfile(userId);
cubit.loadProfile();
}
return Scaffold(
body: BlocBuilder<ProfileCubit, ProfileState>(
body: BlocConsumer<ProfileCubit, ProfileState>(
bloc: cubit,
builder: (context, state) {
if (state.status == ProfileStatus.loading) {
return const Center(child: CircularProgressIndicator());
listener: (context, state) {
if (state.status == ProfileStatus.signedOut) {
Modular.to.navigateToGetStarted();
}
},
builder: (context, state) {
// Show loading spinner if status is loading
if (state.status == ProfileStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == ProfileStatus.error) {
if (state.status == ProfileStatus.error) {
return Center(
child: Text(
state.errorMessage ?? 'An error occurred',
@@ -68,13 +91,10 @@ class StaffProfilePage extends StatelessWidget {
child: Column(
children: [
ProfileHeader(
fullName: profile.fullName,
level: profile.level,
photoUrl: profile.photoUrl,
onSignOutTap: () {
context.read<ProfileCubit>().signOut();
Modular.to.navigateToGetStarted();
},
fullName: profile.name,
level: _mapStatusToLevel(profile.status),
photoUrl: profile.avatar,
onSignOutTap: () => _onSignOut(cubit, state),
),
Transform.translate(
offset: const Offset(0, -24),
@@ -85,42 +105,44 @@ class StaffProfilePage extends StatelessWidget {
child: Column(
children: [
ReliabilityStatsCard(
totalShifts: profile.totalShifts,
averageRating: profile.averageRating,
onTimeRate: profile.onTimeRate,
noShowCount: profile.noShowCount,
cancellationCount: profile.cancellationCount,
totalShifts: profile.totalShifts ?? 0,
averageRating: profile.averageRating ?? 0.0,
onTimeRate: profile.onTimeRate ?? 0,
noShowCount: profile.noShowCount ?? 0,
cancellationCount: profile.cancellationCount ?? 0,
),
const SizedBox(height: UiConstants.space6),
ReliabilityScoreBar(
reliabilityScore: profile.reliabilityScore,
reliabilityScore: profile.reliabilityScore ?? 100,
),
const SizedBox(height: UiConstants.space6),
SectionTitle(i18n.sections.onboarding),
ProfileMenuGrid(
crossAxisCount: 3,
children: [
ProfileMenuItem(
icon: UiIcons.user,
label: i18n.menu_items.personal_info,
completed: profile.hasPersonalInfo,
completed: profile.phone != null,
onTap: () => Modular.to.pushPersonalInfo(),
),
ProfileMenuItem(
icon: UiIcons.phone,
label: i18n.menu_items.emergency_contact,
completed: profile.hasEmergencyContact,
completed: false,
onTap: () => Modular.to.pushEmergencyContact(),
),
ProfileMenuItem(
icon: UiIcons.briefcase,
label: i18n.menu_items.experience,
completed: profile.hasExperience,
completed: false,
onTap: () => Modular.to.pushExperience(),
),
ProfileMenuItem(
icon: UiIcons.user,
label: i18n.menu_items.attire,
completed: profile.hasAttire,
completed: false,
onTap: () => Modular.to.pushAttire(),
),
],
@@ -133,19 +155,19 @@ class StaffProfilePage extends StatelessWidget {
ProfileMenuItem(
icon: UiIcons.file,
label: i18n.menu_items.documents,
completed: profile.hasDocuments,
completed: false,
onTap: () => Modular.to.pushDocuments(),
),
ProfileMenuItem(
icon: UiIcons.shield,
label: i18n.menu_items.certificates,
completed: profile.hasCertificates,
completed: false,
onTap: () => Modular.to.pushCertificates(),
),
ProfileMenuItem(
icon: UiIcons.file,
label: i18n.menu_items.tax_forms,
completed: profile.hasTaxForms,
completed: false,
onTap: () => Modular.to.pushTaxForms(),
),
],
@@ -218,10 +240,7 @@ class StaffProfilePage extends StatelessWidget {
),
const SizedBox(height: UiConstants.space6),
LogoutButton(
onTap: () {
context.read<ProfileCubit>().signOut();
Modular.to.navigateToGetStarted();
},
onTap: () => _onSignOut(cubit, state),
),
],
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'data/repositories/profile_repository_impl.dart';
import 'domain/repositories/profile_repository.dart';
@@ -23,7 +24,10 @@ class StaffProfileModule extends Module {
void binds(Injector i) {
// Repository implementation - delegates to data_connect
i.addLazySingleton<ProfileRepositoryInterface>(
() => ProfileRepositoryImpl(connector: ExampleConnector.instance),
() => ProfileRepositoryImpl(
connector: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// Use cases - depend on repository interface

View File

@@ -36,6 +36,7 @@ dependencies:
path: ../profile_sections/onboarding/emergency_contact
staff_profile_experience:
path: ../profile_sections/onboarding/experience
firebase_auth: ^6.1.4
dev_dependencies:
flutter_test:

View File

@@ -79,7 +79,6 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
status: StaffStatus.active, // TODO: Map from actual status field when available
address: dto.addres,
avatar: dto.photoUrl,
livePhoto: null, // TODO: Map when available in data schema
);
}
}

View File

@@ -33,7 +33,6 @@ class PersonalInfoRepositoryMock implements PersonalInfoRepositoryInterface {
status: StaffStatus.active,
address: 'Montreal, Quebec',
avatar: null,
livePhoto: null,
);
}

View File

@@ -88,7 +88,6 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
status: staff.status,
address: staff.address,
avatar: staff.avatar,
livePhoto: staff.livePhoto,
);
case 'address':
return Staff(
@@ -100,7 +99,6 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
status: staff.status,
address: value,
avatar: staff.avatar,
livePhoto: staff.livePhoto,
);
default:
return staff;