diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart index e5688f29..103b1435 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart @@ -28,7 +28,6 @@ class ProfileRepositoryMock { phone: '555-123-4567', status: StaffStatus.active, avatar: null, - livePhoto: null, address: null, ); } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index f2c3c2d2..3b618cd3 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -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), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart b/apps/mobile/packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart index cf3049fe..c6d2e792 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart @@ -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 getStaffProfile(String userId) async { - // ignore: always_specify_types - final response = await connector.getStaffByUserId(userId: userId).execute(); + Future 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 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 signOut() async { + try { + await firebaseAuth.signOut(); + } catch (e) { + throw Exception('Error signing out: ${e.toString()}'); + } } } diff --git a/apps/mobile/packages/features/staff/profile/lib/src/domain/repositories/profile_repository.dart b/apps/mobile/packages/features/staff/profile/lib/src/domain/repositories/profile_repository.dart index 878933d0..05868bbb 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/domain/repositories/profile_repository.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/domain/repositories/profile_repository.dart @@ -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 getStaffProfile(String userId); + Future getStaffProfile(); /// Signs out the current user. /// diff --git a/apps/mobile/packages/features/staff/profile/lib/src/domain/ui_entities/staff_profile_ui.dart b/apps/mobile/packages/features/staff/profile/lib/src/domain/ui_entities/staff_profile_ui.dart deleted file mode 100644 index 2f306905..00000000 --- a/apps/mobile/packages/features/staff/profile/lib/src/domain/ui_entities/staff_profile_ui.dart +++ /dev/null @@ -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 get props => [ - staff, - totalShifts, - averageRating, - onTimeRate, - noShowCount, - cancellationCount, - reliabilityScore, - hasPersonalInfo, - hasEmergencyContact, - hasExperience, - hasAttire, - hasDocuments, - hasCertificates, - hasTaxForms, - ]; -} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/domain/usecases/get_profile_usecase.dart b/apps/mobile/packages/features/staff/profile/lib/src/domain/usecases/get_profile_usecase.dart index 5288c865..bb1a96d8 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/domain/usecases/get_profile_usecase.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/domain/usecases/get_profile_usecase.dart @@ -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 { +class GetProfileUseCase implements UseCase { final ProfileRepositoryInterface _repository; /// Creates a [GetProfileUseCase]. @@ -26,26 +18,8 @@ class GetProfileUseCase implements UseCase { const GetProfileUseCase(this._repository); @override - Future 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 call([void params]) async { + // Fetch staff object from repository and return directly + return await _repository.getStaffProfile(); } } diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart index 9eb307ff..ea5a5fff 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_cubit.dart @@ -21,13 +21,11 @@ class ProfileCubit extends Cubit { /// 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 loadProfile(String userId) async { + Future 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 { /// Delegates to the sign-out use case which handles session cleanup /// and navigation. Future 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 diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart index 645d241b..05668656 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/blocs/profile_state.dart @@ -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( diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index bea50286..e1c3d3f3 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -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(); // 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( + body: BlocConsumer( 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().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().signOut(); - Modular.to.navigateToGetStarted(); - }, + onTap: () => _onSignOut(cubit, state), ), ], ), diff --git a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart index d9aee76c..014ca130 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart @@ -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( - () => ProfileRepositoryImpl(connector: ExampleConnector.instance), + () => ProfileRepositoryImpl( + connector: ExampleConnector.instance, + firebaseAuth: FirebaseAuth.instance, + ), ); // Use cases - depend on repository interface diff --git a/apps/mobile/packages/features/staff/profile/pubspec.yaml b/apps/mobile/packages/features/staff/profile/pubspec.yaml index 76c872f6..20fd676f 100644 --- a/apps/mobile/packages/features/staff/profile/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile/pubspec.yaml @@ -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: diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart index 1674b42b..bac8463e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart @@ -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 ); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_mock.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_mock.dart index e19847f9..b7b26e44 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_mock.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_mock.dart @@ -33,7 +33,6 @@ class PersonalInfoRepositoryMock implements PersonalInfoRepositoryInterface { status: StaffStatus.active, address: 'Montreal, Quebec', avatar: null, - livePhoto: null, ); } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart index 7bba02b1..36cffabe 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart @@ -88,7 +88,6 @@ class PersonalInfoBloc extends Bloc status: staff.status, address: staff.address, avatar: staff.avatar, - livePhoto: staff.livePhoto, ); case 'address': return Staff( @@ -100,7 +99,6 @@ class PersonalInfoBloc extends Bloc status: staff.status, address: value, avatar: staff.avatar, - livePhoto: staff.livePhoto, ); default: return staff;