feat: Implement Staff Profile feature with BLoC architecture
- Added ProfileRepositoryInterface for staff profile operations. - Created StaffProfileUI to represent extended profile information for the UI. - Developed GetProfileUseCase to fetch staff profiles and map to UI entities. - Implemented SignOutUseCase for user sign-out functionality. - Introduced ProfileCubit to manage profile state and loading logic. - Created ProfileState to represent various states of the profile feature. - Developed ProfileNavigator for type-safe navigation within the profile feature. - Built StaffProfilePage to display staff member's profile and statistics. - Added various widgets for profile display, including reliability stats and menu items. - Established StaffProfileModule for dependency injection and routing. - Configured pubspec.yaml for feature package dependencies.
This commit is contained in:
@@ -453,15 +453,51 @@
|
||||
"refer": { "title": "Refer a Friend", "page": "/worker-profile" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"staff_main": {
|
||||
"tabs": {
|
||||
"profile": {
|
||||
"header": {
|
||||
"title": "Profile",
|
||||
"sign_out": "SIGN OUT"
|
||||
},
|
||||
"reliability_stats": {
|
||||
"shifts": "Shifts",
|
||||
"rating": "Rating",
|
||||
"on_time": "On Time",
|
||||
"no_shows": "No Shows",
|
||||
"cancellations": "Cancel."
|
||||
},
|
||||
"reliability_score": {
|
||||
"title": "Reliability Score",
|
||||
"description": "Keep your score above 45% to continue picking up shifts."
|
||||
},
|
||||
"sections": {
|
||||
"onboarding": "ONBOARDING",
|
||||
"compliance": "COMPLIANCE",
|
||||
"level_up": "LEVEL UP",
|
||||
"finance": "FINANCE",
|
||||
"support": "SUPPORT"
|
||||
},
|
||||
"menu_items": {
|
||||
"personal_info": "Personal Info",
|
||||
"emergency_contact": "Emergency Contact",
|
||||
"experience": "Experience",
|
||||
"attire": "Attire",
|
||||
"documents": "Documents",
|
||||
"certificates": "Certificates",
|
||||
"tax_forms": "Tax Forms",
|
||||
"krow_university": "Krow University",
|
||||
"trainings": "Trainings",
|
||||
"leaderboard": "Leaderboard",
|
||||
"bank_account": "Bank Account",
|
||||
"payments": "Payments",
|
||||
"home": "Home",
|
||||
"clock_in": "Clock In",
|
||||
"profile": "Profile"
|
||||
"timecard": "Timecard",
|
||||
"faqs": "FAQs",
|
||||
"privacy_security": "Privacy & Security",
|
||||
"messages": "Messages"
|
||||
},
|
||||
"logout": {
|
||||
"button": "Sign Out"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ library;
|
||||
|
||||
export 'src/mocks/auth_repository_mock.dart';
|
||||
export 'src/mocks/staff_repository_mock.dart';
|
||||
export 'src/mocks/profile_repository_mock.dart';
|
||||
export 'src/mocks/event_repository_mock.dart';
|
||||
export 'src/mocks/skill_repository_mock.dart';
|
||||
export 'src/mocks/financial_repository_mock.dart';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Mock implementation of profile-related data operations.
|
||||
///
|
||||
/// This mock provides hardcoded staff profile data for development and testing.
|
||||
/// It simulates the behavior of a real backend service without requiring
|
||||
/// actual API calls or Firebase Data Connect.
|
||||
///
|
||||
/// In production, this will be replaced with a real implementation that
|
||||
/// interacts with Firebase Data Connect.
|
||||
class ProfileRepositoryMock {
|
||||
/// Fetches the staff profile for the given user ID.
|
||||
///
|
||||
/// Returns a [Staff] entity with mock data.
|
||||
/// Simulates a network delay to mirror real API behavior.
|
||||
///
|
||||
/// Throws an exception if the profile cannot be retrieved.
|
||||
Future<Staff> getStaffProfile(String userId) async {
|
||||
// Simulate network delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Return mock staff profile data
|
||||
return const Staff(
|
||||
id: '93673c8f-91aa-405d-8647-f1aac29cc19b',
|
||||
authProviderId: 't8P3fYh4y1cPoZbbVPXUhfQCsDo3',
|
||||
name: 'Krower',
|
||||
email: 'worker@krow.com',
|
||||
phone: '555-123-4567',
|
||||
status: StaffStatus.active,
|
||||
avatar: null,
|
||||
livePhoto: null,
|
||||
address: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Signs out the current user.
|
||||
///
|
||||
/// Simulates the sign-out process with a delay.
|
||||
/// In a real implementation, this would clear session tokens,
|
||||
/// update Firebase auth state, etc.
|
||||
Future<void> signOut() async {
|
||||
// Simulate processing delay
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/profile_repository.dart';
|
||||
|
||||
/// Implementation of [ProfileRepositoryInterface] that delegates to data_connect.
|
||||
///
|
||||
/// This implementation follows Clean Architecture by:
|
||||
/// - Implementing the domain layer's repository interface
|
||||
/// - Delegating all data access to the data_connect package
|
||||
/// - Not containing any business logic
|
||||
/// - Only performing data transformation/mapping if needed
|
||||
///
|
||||
/// Currently uses [ProfileRepositoryMock] from data_connect.
|
||||
/// When Firebase Data Connect is ready, this will be swapped with a real implementation.
|
||||
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
|
||||
final ProfileRepositoryMock _dataConnectRepository;
|
||||
|
||||
/// Creates a [ProfileRepositoryImpl].
|
||||
///
|
||||
/// Requires a [ProfileRepositoryMock] from the data_connect package.
|
||||
const ProfileRepositoryImpl(this._dataConnectRepository);
|
||||
|
||||
@override
|
||||
Future<Staff> getStaffProfile(String userId) {
|
||||
// Delegate directly to data_connect - no business logic here
|
||||
return _dataConnectRepository.getStaffProfile(userId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() {
|
||||
// Delegate directly to data_connect - no business logic here
|
||||
return _dataConnectRepository.signOut();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for staff profile operations.
|
||||
///
|
||||
/// Defines the contract for accessing and managing staff profile data.
|
||||
/// This interface lives in the domain layer and is implemented by the data layer.
|
||||
///
|
||||
/// Following Clean Architecture principles, this interface:
|
||||
/// - Returns domain entities (Staff from shared domain package)
|
||||
/// - 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.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
/// Signs out the current user.
|
||||
///
|
||||
/// Clears the user's session and authentication state.
|
||||
/// Should be followed by navigation to the authentication flow.
|
||||
Future<void> signOut();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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
|
||||
///
|
||||
/// 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> {
|
||||
final ProfileRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [GetProfileUseCase].
|
||||
///
|
||||
/// Requires a [ProfileRepositoryInterface] to interact with the profile data source.
|
||||
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
|
||||
// TODO: Replace mock data with actual profile statistics from backend
|
||||
return StaffProfileUI(
|
||||
staff: staff,
|
||||
totalShifts: 0,
|
||||
averageRating: 5.0,
|
||||
onTimeRate: 100,
|
||||
noShowCount: 0,
|
||||
cancellationCount: 0,
|
||||
reliabilityScore: 100,
|
||||
hasPersonalInfo: staff.phone != null && staff.phone!.isNotEmpty,
|
||||
hasEmergencyContact: false, // TODO: Fetch from backend
|
||||
hasExperience: false, // TODO: Fetch from backend
|
||||
hasAttire: staff.avatar != null,
|
||||
hasDocuments: false, // TODO: Fetch from backend
|
||||
hasCertificates: false, // TODO: Fetch from backend
|
||||
hasTaxForms: false, // TODO: Fetch from backend
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Use case for signing out the current user.
|
||||
///
|
||||
/// This use case delegates the sign-out logic to the [ProfileRepositoryInterface].
|
||||
///
|
||||
/// Following Clean Architecture principles, this use case:
|
||||
/// - Encapsulates the sign-out business rule
|
||||
/// - Depends only on the repository interface
|
||||
/// - Keeps the domain layer independent of external frameworks
|
||||
class SignOutUseCase implements NoInputUseCase<void> {
|
||||
final ProfileRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [SignOutUseCase].
|
||||
///
|
||||
/// Requires a [ProfileRepositoryInterface] to perform the sign-out operation.
|
||||
const SignOutUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<void> call() {
|
||||
return _repository.signOut();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/get_profile_usecase.dart';
|
||||
import '../../domain/usecases/sign_out_usecase.dart';
|
||||
import 'profile_state.dart';
|
||||
|
||||
/// Cubit for managing the Profile feature state.
|
||||
///
|
||||
/// Handles loading profile data and user sign-out actions.
|
||||
class ProfileCubit extends Cubit<ProfileState> {
|
||||
final GetProfileUseCase _getProfileUseCase;
|
||||
final SignOutUseCase _signOutUseCase;
|
||||
|
||||
/// Creates a [ProfileCubit] with the required use cases.
|
||||
ProfileCubit(
|
||||
this._getProfileUseCase,
|
||||
this._signOutUseCase,
|
||||
) : super(const ProfileState());
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Requires [userId] to identify which profile to load.
|
||||
Future<void> loadProfile(String userId) async {
|
||||
emit(state.copyWith(status: ProfileStatus.loading));
|
||||
|
||||
try {
|
||||
final profile = await _getProfileUseCase(userId);
|
||||
emit(state.copyWith(
|
||||
status: ProfileStatus.loaded,
|
||||
profile: profile,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ProfileStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs out the current user.
|
||||
///
|
||||
/// Delegates to the sign-out use case which handles session cleanup
|
||||
/// and navigation.
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _signOutUseCase();
|
||||
} catch (e) {
|
||||
// Error handling can be added here if needed
|
||||
// For now, we let the navigation happen regardless
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../domain/ui_entities/staff_profile_ui.dart';
|
||||
|
||||
/// Represents the various states of the profile feature.
|
||||
enum ProfileStatus {
|
||||
/// Initial state before any data is loaded
|
||||
initial,
|
||||
|
||||
/// Profile data is being loaded
|
||||
loading,
|
||||
|
||||
/// Profile data loaded successfully
|
||||
loaded,
|
||||
|
||||
/// An error occurred while loading profile data
|
||||
error,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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;
|
||||
|
||||
/// Error message if status is error
|
||||
final String? errorMessage;
|
||||
|
||||
const ProfileState({
|
||||
this.status = ProfileStatus.initial,
|
||||
this.profile,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Creates a copy of this state with updated values.
|
||||
ProfileState copyWith({
|
||||
ProfileStatus? status,
|
||||
StaffProfileUI? profile,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return ProfileState(
|
||||
status: status ?? this.status,
|
||||
profile: profile ?? this.profile,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, profile, errorMessage];
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
/// Extension on [IModularNavigator] providing typed navigation helpers
|
||||
/// for the Staff Profile feature.
|
||||
///
|
||||
/// These methods provide a type-safe way to navigate to various profile-related
|
||||
/// pages without relying on string literals throughout the codebase.
|
||||
extension ProfileNavigator on IModularNavigator {
|
||||
/// Navigates to the personal info page.
|
||||
void pushPersonalInfo() {
|
||||
pushNamed('/personal-info');
|
||||
}
|
||||
|
||||
/// Navigates to the emergency contact page.
|
||||
void pushEmergencyContact() {
|
||||
pushNamed('/emergency-contact');
|
||||
}
|
||||
|
||||
/// Navigates to the experience page.
|
||||
void pushExperience() {
|
||||
pushNamed('/experience');
|
||||
}
|
||||
|
||||
/// Navigates to the attire page.
|
||||
void pushAttire() {
|
||||
pushNamed('/attire');
|
||||
}
|
||||
|
||||
/// Navigates to the documents page.
|
||||
void pushDocuments() {
|
||||
pushNamed('/documents');
|
||||
}
|
||||
|
||||
/// Navigates to the certificates page.
|
||||
void pushCertificates() {
|
||||
pushNamed('/certificates');
|
||||
}
|
||||
|
||||
/// Navigates to the tax forms page.
|
||||
void pushTaxForms() {
|
||||
pushNamed('/tax-forms');
|
||||
}
|
||||
|
||||
/// Navigates to Krow University.
|
||||
void pushKrowUniversity() {
|
||||
pushNamed('/krow-university');
|
||||
}
|
||||
|
||||
/// Navigates to the trainings page.
|
||||
void pushTrainings() {
|
||||
pushNamed('/trainings');
|
||||
}
|
||||
|
||||
/// Navigates to the leaderboard page.
|
||||
void pushLeaderboard() {
|
||||
pushNamed('/leaderboard');
|
||||
}
|
||||
|
||||
/// Navigates to the bank account page.
|
||||
void pushBankAccount() {
|
||||
pushNamed('/bank-account');
|
||||
}
|
||||
|
||||
/// Navigates to the timecard page.
|
||||
void pushTimecard() {
|
||||
pushNamed('/time-card');
|
||||
}
|
||||
|
||||
/// Navigates to the FAQs page.
|
||||
void pushFaqs() {
|
||||
pushNamed('/faqs');
|
||||
}
|
||||
|
||||
/// Navigates to the privacy & security page.
|
||||
void pushPrivacy() {
|
||||
pushNamed('/privacy');
|
||||
}
|
||||
|
||||
/// Navigates to the messages page.
|
||||
void pushMessages() {
|
||||
pushNamed('/messages');
|
||||
}
|
||||
|
||||
/// Navigates to the get started/authentication screen.
|
||||
void navigateToGetStarted() {
|
||||
navigate('/get-started/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart' hide ReadContext;
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
import '../blocs/profile_cubit.dart';
|
||||
import '../blocs/profile_state.dart';
|
||||
import '../navigation/profile_navigator.dart';
|
||||
import '../widgets/logout_button.dart';
|
||||
import '../widgets/profile_menu_grid.dart';
|
||||
import '../widgets/profile_menu_item.dart';
|
||||
import '../widgets/profile_header.dart';
|
||||
import '../widgets/reliability_score_bar.dart';
|
||||
import '../widgets/reliability_stats_card.dart';
|
||||
import '../widgets/section_title.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.
|
||||
class StaffProfilePage extends StatelessWidget {
|
||||
/// Creates a [StaffProfilePage].
|
||||
const StaffProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.profile;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) {
|
||||
// TODO: Get actual userId from auth session
|
||||
// For now, using mock userId that matches ProfileRepositoryMock data
|
||||
const userId = 't8P3fYh4y1cPoZbbVPXUhfQCsDo3';
|
||||
return Modular.get<ProfileCubit>()..loadProfile(userId);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: BlocBuilder<ProfileCubit, ProfileState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ProfileStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.status == ProfileStatus.error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.errorMessage ?? 'An error occurred',
|
||||
style: UiTypography.body1r.copyWith(
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final profile = state.profile;
|
||||
if (profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
ProfileHeader(
|
||||
fullName: profile.fullName,
|
||||
level: profile.level,
|
||||
photoUrl: profile.photoUrl,
|
||||
onSignOutTap: () {
|
||||
context.read<ProfileCubit>().signOut();
|
||||
Modular.to.navigateToGetStarted();
|
||||
},
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -24),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ReliabilityStatsCard(
|
||||
totalShifts: profile.totalShifts,
|
||||
averageRating: profile.averageRating,
|
||||
onTimeRate: profile.onTimeRate,
|
||||
noShowCount: profile.noShowCount,
|
||||
cancellationCount: profile.cancellationCount,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ReliabilityScoreBar(
|
||||
reliabilityScore: profile.reliabilityScore,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.onboarding),
|
||||
ProfileMenuGrid(
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.user,
|
||||
label: i18n.menu_items.personal_info,
|
||||
completed: profile.hasPersonalInfo,
|
||||
onTap: () => Modular.to.pushPersonalInfo(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.phone,
|
||||
label: i18n.menu_items.emergency_contact,
|
||||
completed: profile.hasEmergencyContact,
|
||||
onTap: () => Modular.to.pushEmergencyContact(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.briefcase,
|
||||
label: i18n.menu_items.experience,
|
||||
completed: profile.hasExperience,
|
||||
onTap: () => Modular.to.pushExperience(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.shirt,
|
||||
label: i18n.menu_items.attire,
|
||||
completed: profile.hasAttire,
|
||||
onTap: () => Modular.to.pushAttire(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.compliance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.fileText,
|
||||
label: i18n.menu_items.documents,
|
||||
completed: profile.hasDocuments,
|
||||
onTap: () => Modular.to.pushDocuments(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.award,
|
||||
label: i18n.menu_items.certificates,
|
||||
completed: profile.hasCertificates,
|
||||
onTap: () => Modular.to.pushCertificates(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.fileText,
|
||||
label: i18n.menu_items.tax_forms,
|
||||
completed: profile.hasTaxForms,
|
||||
onTap: () => Modular.to.pushTaxForms(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.level_up),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.graduationCap,
|
||||
label: i18n.menu_items.krow_university,
|
||||
onTap: () => Modular.to.pushKrowUniversity(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.bookOpen,
|
||||
label: i18n.menu_items.trainings,
|
||||
onTap: () => Modular.to.pushTrainings(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.award,
|
||||
label: i18n.menu_items.leaderboard,
|
||||
onTap: () => Modular.to.pushLeaderboard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.finance),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.building2,
|
||||
label: i18n.menu_items.bank_account,
|
||||
onTap: () => Modular.to.pushBankAccount(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.creditCard,
|
||||
label: i18n.menu_items.payments,
|
||||
onTap: () => Modular.to.navigate('/payments'),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.clock,
|
||||
label: i18n.menu_items.timecard,
|
||||
onTap: () => Modular.to.pushTimecard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SectionTitle(i18n.sections.support),
|
||||
ProfileMenuGrid(
|
||||
crossAxisCount: 3,
|
||||
children: [
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.helpCircle,
|
||||
label: i18n.menu_items.faqs,
|
||||
onTap: () => Modular.to.pushFaqs(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.shield,
|
||||
label: i18n.menu_items.privacy_security,
|
||||
onTap: () => Modular.to.pushPrivacy(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: LucideIcons.messageCircle,
|
||||
label: i18n.menu_items.messages,
|
||||
onTap: () => Modular.to.pushMessages(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
LogoutButton(
|
||||
onTap: () {
|
||||
context.read<ProfileCubit>().signOut();
|
||||
Modular.to.navigateToGetStarted();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// NOTE: This is a TEMPORARY class to allow the prototype code to compile
|
||||
/// without immediate design system integration.
|
||||
/// It will be replaced by the actual Design System tokens (UiColors) in Step 4.
|
||||
class AppColors {
|
||||
static const Color krowBackground = Color(0xFFF9F9F9);
|
||||
static const Color krowBlue = Color(0xFF0055FF);
|
||||
static const Color krowYellow = Color(0xFFFFCC00);
|
||||
static const Color krowBorder = Color(0xFFE0E0E0);
|
||||
static const Color krowCharcoal = Color(0xFF333333);
|
||||
static const Color krowMuted = Color(0xFF808080);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// The sign-out button widget.
|
||||
///
|
||||
/// Uses design system tokens for all colors, typography, spacing, and icons.
|
||||
class LogoutButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
|
||||
const LogoutButton({super.key, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.profile.header;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(LucideIcons.logOut, color: UiColors.destructive, size: 20),
|
||||
SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
i18n.sign_out,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// The header section of the staff profile page, containing avatar, name, level,
|
||||
/// and a sign-out button.
|
||||
///
|
||||
/// Uses design system tokens for all colors, typography, and spacing.
|
||||
class ProfileHeader extends StatelessWidget {
|
||||
/// The staff member's full name
|
||||
final String fullName;
|
||||
|
||||
/// The staff member's level (e.g., "Krower I")
|
||||
final String level;
|
||||
|
||||
/// Optional photo URL for the avatar
|
||||
final String? photoUrl;
|
||||
|
||||
/// Callback when sign out is tapped
|
||||
final VoidCallback onSignOutTap;
|
||||
|
||||
/// Creates a [ProfileHeader].
|
||||
const ProfileHeader({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
required this.level,
|
||||
this.photoUrl,
|
||||
required this.onSignOutTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.profile.header;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(UiConstants.space6),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
// Top Bar
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.primaryForeground,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onSignOutTap,
|
||||
child: Text(
|
||||
i18n.sign_out,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primaryForeground.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space8),
|
||||
// Avatar Section
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Container(
|
||||
width: 112,
|
||||
height: 112,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
UiColors.accent,
|
||||
UiColors.accent.withOpacity(0.5),
|
||||
UiColors.primaryForeground,
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: UiColors.primaryForeground.withOpacity(0.2),
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: UiColors.background,
|
||||
backgroundImage: photoUrl != null
|
||||
? NetworkImage(photoUrl!)
|
||||
: null,
|
||||
child: photoUrl == null
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
UiColors.accent,
|
||||
UiColors.accent.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'K',
|
||||
style: UiTypography.displayM.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primaryForeground,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: UiColors.primary, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.camera,
|
||||
size: 16,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
fullName,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.primaryForeground,
|
||||
),
|
||||
),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.space5),
|
||||
),
|
||||
child: Text(
|
||||
level,
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// Lays out a list of widgets (intended for [ProfileMenuItem]s) in a responsive grid.
|
||||
/// It uses [Wrap] and manually calculates item width based on the screen size.
|
||||
class ProfileMenuGrid extends StatelessWidget {
|
||||
final int crossAxisCount;
|
||||
final List<Widget> children;
|
||||
|
||||
const ProfileMenuGrid({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.crossAxisCount = 2,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Spacing between items
|
||||
final double spacing = UiConstants.space3;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final totalWidth = constraints.maxWidth;
|
||||
final totalSpacingWidth = spacing * (crossAxisCount - 1);
|
||||
final itemWidth = (totalWidth - totalSpacingWidth) / crossAxisCount;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: children.map((child) {
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
child: child,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// An individual item within the profile menu grid.
|
||||
///
|
||||
/// Uses design system tokens for all colors, typography, spacing, and borders.
|
||||
class ProfileMenuItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool? completed;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ProfileMenuItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.completed,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
padding: EdgeInsets.all(UiConstants.space2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: UiColors.primary, size: 24),
|
||||
),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: UiConstants.space1),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
color: UiColors.foreground,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (completed != null)
|
||||
Positioned(
|
||||
top: UiConstants.space2,
|
||||
right: UiConstants.space2,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: completed!
|
||||
? UiColors.primary
|
||||
: UiColors.primary.withOpacity(0.1),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: completed!
|
||||
? const Icon(Icons.check, size: 10, color: UiColors.primaryForeground)
|
||||
: Text(
|
||||
"!",
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// Displays the staff member's reliability score as a progress bar.
|
||||
///
|
||||
/// Uses design system tokens for all colors, typography, and spacing.
|
||||
class ReliabilityScoreBar extends StatelessWidget {
|
||||
final int reliabilityScore;
|
||||
|
||||
const ReliabilityScoreBar({
|
||||
super.key,
|
||||
required this.reliabilityScore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.profile.reliability_score;
|
||||
final score = reliabilityScore / 100;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$reliabilityScore%",
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(UiConstants.space1),
|
||||
child: LinearProgressIndicator(
|
||||
value: score,
|
||||
backgroundColor: UiColors.background,
|
||||
color: UiColors.primary,
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: UiConstants.space2),
|
||||
child: Text(
|
||||
i18n.description,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// Displays the staff member's reliability statistics (Shifts, Rating, On Time, etc.).
|
||||
///
|
||||
/// Uses design system tokens for all colors, typography, spacing, and icons.
|
||||
class ReliabilityStatsCard extends StatelessWidget {
|
||||
final int totalShifts;
|
||||
final double averageRating;
|
||||
final int onTimeRate;
|
||||
final int noShowCount;
|
||||
final int cancellationCount;
|
||||
|
||||
const ReliabilityStatsCard({
|
||||
super.key,
|
||||
required this.totalShifts,
|
||||
required this.averageRating,
|
||||
required this.onTimeRate,
|
||||
required this.noShowCount,
|
||||
required this.cancellationCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.briefcase,
|
||||
"$totalShifts",
|
||||
"Shifts",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.star,
|
||||
averageRating.toStringAsFixed(1),
|
||||
"Rating",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.clock,
|
||||
"$onTimeRate%",
|
||||
"On Time",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.xCircle,
|
||||
"$noShowCount",
|
||||
"No Shows",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.ban,
|
||||
"$cancellationCount",
|
||||
"Cancel.",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String value,
|
||||
String label,
|
||||
) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, size: 20, color: UiColors.primary),
|
||||
),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
/// Displays a capitalized, muted section title.
|
||||
///
|
||||
/// Uses design system tokens for typography, colors, and spacing.
|
||||
class SectionTitle extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const SectionTitle(this.title, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: UiConstants.space1),
|
||||
margin: EdgeInsets.only(bottom: UiConstants.space3),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
import 'data/repositories/profile_repository_impl.dart';
|
||||
import 'domain/repositories/profile_repository.dart';
|
||||
import 'domain/usecases/get_profile_usecase.dart';
|
||||
import 'domain/usecases/sign_out_usecase.dart';
|
||||
import 'presentation/blocs/profile_cubit.dart';
|
||||
import '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:
|
||||
/// - Data source (ProfileRepositoryMock) from data_connect package
|
||||
/// - Repository implementation (ProfileRepositoryImpl) delegates to data_connect
|
||||
/// - Use cases depend on repository interface
|
||||
/// - Cubit depends on use cases
|
||||
class StaffProfileModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Data layer - Get mock from data_connect package
|
||||
i.addLazySingleton<ProfileRepositoryMock>(ProfileRepositoryMock.new);
|
||||
|
||||
// Repository implementation - delegates to data_connect
|
||||
i.addLazySingleton<ProfileRepositoryInterface>(
|
||||
() => ProfileRepositoryImpl(i.get<ProfileRepositoryMock>()),
|
||||
);
|
||||
|
||||
// Use cases - depend on repository interface
|
||||
i.addLazySingleton<GetProfileUseCase>(
|
||||
() => GetProfileUseCase(i.get<ProfileRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton<SignOutUseCase>(
|
||||
() => SignOutUseCase(i.get<ProfileRepositoryInterface>()),
|
||||
);
|
||||
|
||||
// Presentation layer - Cubit depends on use cases
|
||||
i.addSingleton(
|
||||
() => ProfileCubit(
|
||||
i.get<GetProfileUseCase>(),
|
||||
i.get<SignOutUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// Export the modular feature definition.
|
||||
export 'src/staff_profile_module.dart';
|
||||
41
apps/mobile/packages/features/staff/profile/pubspec.yaml
Normal file
41
apps/mobile/packages/features/staff/profile/pubspec.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: staff_profile
|
||||
description: Staff Profile feature package.
|
||||
version: 0.0.1
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# Architecture
|
||||
flutter_modular: ^5.0.0
|
||||
flutter_bloc: ^8.1.3
|
||||
|
||||
# Utility/DI
|
||||
injectable: ^2.3.0
|
||||
get_it: ^7.6.4
|
||||
|
||||
# Project-specific packages
|
||||
domain:
|
||||
path: ../../../domain
|
||||
data_connect:
|
||||
path: ../../../data_connect
|
||||
core_localization:
|
||||
path: ../../../core_localization
|
||||
design_system:
|
||||
path: ../../../design_system # Assuming this path
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
injectable_generator: ^2.4.1
|
||||
build_runner: ^2.4.6
|
||||
|
||||
# Flutter modular configuration
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
Reference in New Issue
Block a user