feat: Add experience management feature with UI, BLoC integration, and repository implementation
This commit is contained in:
@@ -524,6 +524,42 @@
|
|||||||
"locations_hint": "Downtown, Midtown, Brooklyn...",
|
"locations_hint": "Downtown, Midtown, Brooklyn...",
|
||||||
"save_button": "Save Changes",
|
"save_button": "Save Changes",
|
||||||
"save_success": "Personal info saved successfully"
|
"save_success": "Personal info saved successfully"
|
||||||
|
},
|
||||||
|
"experience": {
|
||||||
|
"title": "Experience & Skills",
|
||||||
|
"industries_title": "Industries",
|
||||||
|
"industries_subtitle": "Select the industries you have experience in",
|
||||||
|
"skills_title": "Skills",
|
||||||
|
"skills_subtitle": "Select your skills or add custom ones",
|
||||||
|
"custom_skills_title": "Custom Skills:",
|
||||||
|
"custom_skill_hint": "Add custom skill...",
|
||||||
|
"save_button": "Save & Continue",
|
||||||
|
"industries": {
|
||||||
|
"hospitality": "Hospitality",
|
||||||
|
"food_service": "Food Service",
|
||||||
|
"warehouse": "Warehouse",
|
||||||
|
"events": "Events",
|
||||||
|
"retail": "Retail",
|
||||||
|
"healthcare": "Healthcare",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"food_service": "Food Service",
|
||||||
|
"bartending": "Bartending",
|
||||||
|
"event_setup": "Event Setup",
|
||||||
|
"hospitality": "Hospitality",
|
||||||
|
"warehouse": "Warehouse",
|
||||||
|
"customer_service": "Customer Service",
|
||||||
|
"cleaning": "Cleaning",
|
||||||
|
"security": "Security",
|
||||||
|
"retail": "Retail",
|
||||||
|
"cooking": "Cooking",
|
||||||
|
"cashier": "Cashier",
|
||||||
|
"server": "Server",
|
||||||
|
"barista": "Barista",
|
||||||
|
"host_hostess": "Host/Hostess",
|
||||||
|
"busser": "Busser"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'mocks/auth_repository_mock.dart';
|
|||||||
import 'mocks/business_repository_mock.dart';
|
import 'mocks/business_repository_mock.dart';
|
||||||
import 'mocks/home_repository_mock.dart';
|
import 'mocks/home_repository_mock.dart';
|
||||||
import 'mocks/order_repository_mock.dart';
|
import 'mocks/order_repository_mock.dart';
|
||||||
|
import 'mocks/profile_repository_mock.dart';
|
||||||
|
|
||||||
/// A module that provides Data Connect dependencies, including mocks.
|
/// A module that provides Data Connect dependencies, including mocks.
|
||||||
class DataConnectModule extends Module {
|
class DataConnectModule extends Module {
|
||||||
@@ -10,6 +11,7 @@ class DataConnectModule extends Module {
|
|||||||
void exportedBinds(Injector i) {
|
void exportedBinds(Injector i) {
|
||||||
// Make these mocks available to any module that imports this one.
|
// Make these mocks available to any module that imports this one.
|
||||||
i.addLazySingleton(AuthRepositoryMock.new);
|
i.addLazySingleton(AuthRepositoryMock.new);
|
||||||
|
i.addLazySingleton(ProfileRepositoryMock.new);
|
||||||
i.addLazySingleton(HomeRepositoryMock.new);
|
i.addLazySingleton(HomeRepositoryMock.new);
|
||||||
i.addLazySingleton(BusinessRepositoryMock.new);
|
i.addLazySingleton(BusinessRepositoryMock.new);
|
||||||
i.addLazySingleton(OrderRepositoryMock.new);
|
i.addLazySingleton(OrderRepositoryMock.new);
|
||||||
|
|||||||
@@ -65,4 +65,26 @@ class ProfileRepositoryMock {
|
|||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
// Simulate save
|
// Simulate save
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches selected industries for the given staff ID.
|
||||||
|
Future<List<String>> getStaffIndustries(String staffId) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
return ['hospitality', 'events'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches selected skills for the given staff ID.
|
||||||
|
Future<List<String>> getStaffSkills(String staffId) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
return ['Bartending', 'Server'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves experience (industries and skills) for the given staff ID.
|
||||||
|
Future<void> saveExperience(
|
||||||
|
String staffId,
|
||||||
|
List<String> industries,
|
||||||
|
List<String> skills,
|
||||||
|
) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
// Simulate save
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ extension ProfileNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the experience page.
|
/// Navigates to the experience page.
|
||||||
void pushExperience() {
|
void pushExperience() {
|
||||||
pushNamed('/profile/onboarding/experience');
|
pushNamed('/profile/experience');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the attire page.
|
/// Navigates to the attire page.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:staff_profile_info/staff_profile_info.dart';
|
import 'package:staff_profile_info/staff_profile_info.dart';
|
||||||
import 'package:staff_emergency_contact/staff_emergency_contact.dart';
|
import 'package:staff_emergency_contact/staff_emergency_contact.dart';
|
||||||
|
import 'package:staff_profile_experience/staff_profile_experience.dart';
|
||||||
|
|
||||||
import 'data/repositories/profile_repository_impl.dart';
|
import 'data/repositories/profile_repository_impl.dart';
|
||||||
import 'domain/repositories/profile_repository.dart';
|
import 'domain/repositories/profile_repository.dart';
|
||||||
@@ -55,5 +56,6 @@ class StaffProfileModule extends Module {
|
|||||||
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
||||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||||
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||||
|
r.module('/experience', module: StaffProfileExperienceModule());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ dependencies:
|
|||||||
path: ../profile_sections/onboarding/profile_info
|
path: ../profile_sections/onboarding/profile_info
|
||||||
staff_emergency_contact:
|
staff_emergency_contact:
|
||||||
path: ../profile_sections/onboarding/emergency_contact
|
path: ../profile_sections/onboarding/emergency_contact
|
||||||
|
staff_profile_experience:
|
||||||
|
path: ../profile_sections/onboarding/experience
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# include: package:flutter_lints/flutter.yaml
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
import '../../domain/repositories/experience_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Implementation of [ExperienceRepositoryInterface] that delegates to Data Connect.
|
||||||
|
class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||||
|
final ProfileRepositoryMock _mockRepository;
|
||||||
|
|
||||||
|
/// Creates a [ExperienceRepositoryImpl] with the given [ProfileRepositoryMock].
|
||||||
|
ExperienceRepositoryImpl(this._mockRepository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getIndustries(String staffId) {
|
||||||
|
return _mockRepository.getStaffIndustries(staffId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getSkills(String staffId) {
|
||||||
|
return _mockRepository.getStaffSkills(staffId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveExperience(
|
||||||
|
String staffId,
|
||||||
|
List<String> industries,
|
||||||
|
List<String> skills,
|
||||||
|
) {
|
||||||
|
return _mockRepository.saveExperience(staffId, industries, skills);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
class GetExperienceArguments extends UseCaseArgument {
|
||||||
|
final String staffId;
|
||||||
|
|
||||||
|
GetExperienceArguments({required this.staffId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [staffId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
class SaveExperienceArguments extends UseCaseArgument {
|
||||||
|
final String staffId;
|
||||||
|
final List<String> industries;
|
||||||
|
final List<String> skills;
|
||||||
|
|
||||||
|
SaveExperienceArguments({
|
||||||
|
required this.staffId,
|
||||||
|
required this.industries,
|
||||||
|
required this.skills,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [staffId, industries, skills];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/// Interface for accessing staff experience data.
|
||||||
|
abstract class ExperienceRepositoryInterface {
|
||||||
|
/// Fetches the list of industries associated with the staff member.
|
||||||
|
Future<List<String>> getIndustries(String staffId);
|
||||||
|
|
||||||
|
/// Fetches the list of skills associated with the staff member.
|
||||||
|
Future<List<String>> getSkills(String staffId);
|
||||||
|
|
||||||
|
/// Saves the staff member's experience (industries and skills).
|
||||||
|
Future<void> saveExperience(
|
||||||
|
String staffId,
|
||||||
|
List<String> industries,
|
||||||
|
List<String> skills,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../arguments/get_experience_arguments.dart';
|
||||||
|
import '../repositories/experience_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case for fetching staff industries.
|
||||||
|
class GetStaffIndustriesUseCase implements UseCase<GetExperienceArguments, List<String>> {
|
||||||
|
final ExperienceRepositoryInterface _repository;
|
||||||
|
|
||||||
|
GetStaffIndustriesUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> call(GetExperienceArguments input) {
|
||||||
|
return _repository.getIndustries(input.staffId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../arguments/get_experience_arguments.dart';
|
||||||
|
import '../repositories/experience_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case for fetching staff skills.
|
||||||
|
class GetStaffSkillsUseCase implements UseCase<GetExperienceArguments, List<String>> {
|
||||||
|
final ExperienceRepositoryInterface _repository;
|
||||||
|
|
||||||
|
GetStaffSkillsUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> call(GetExperienceArguments input) {
|
||||||
|
return _repository.getSkills(input.staffId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../arguments/save_experience_arguments.dart';
|
||||||
|
import '../repositories/experience_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case for saving staff experience details.
|
||||||
|
///
|
||||||
|
/// Delegates the saving logic to [ExperienceRepositoryInterface].
|
||||||
|
class SaveExperienceUseCase extends UseCase<SaveExperienceArguments, void> {
|
||||||
|
final ExperienceRepositoryInterface repository;
|
||||||
|
|
||||||
|
/// Creates a [SaveExperienceUseCase].
|
||||||
|
SaveExperienceUseCase(this.repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> call(SaveExperienceArguments params) {
|
||||||
|
return repository.saveExperience(
|
||||||
|
params.staffId,
|
||||||
|
params.industries,
|
||||||
|
params.skills,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../domain/arguments/get_experience_arguments.dart';
|
||||||
|
import '../../domain/arguments/save_experience_arguments.dart';
|
||||||
|
import '../../domain/usecases/get_staff_industries_usecase.dart';
|
||||||
|
import '../../domain/usecases/get_staff_skills_usecase.dart';
|
||||||
|
import '../../domain/usecases/save_experience_usecase.dart';
|
||||||
|
|
||||||
|
// Events
|
||||||
|
abstract class ExperienceEvent extends Equatable {
|
||||||
|
const ExperienceEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExperienceLoaded extends ExperienceEvent {}
|
||||||
|
|
||||||
|
class ExperienceIndustryToggled extends ExperienceEvent {
|
||||||
|
final String industry;
|
||||||
|
const ExperienceIndustryToggled(this.industry);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [industry];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExperienceSkillToggled extends ExperienceEvent {
|
||||||
|
final String skill;
|
||||||
|
const ExperienceSkillToggled(this.skill);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [skill];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExperienceCustomSkillAdded extends ExperienceEvent {
|
||||||
|
final String skill;
|
||||||
|
const ExperienceCustomSkillAdded(this.skill);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [skill];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExperienceSubmitted extends ExperienceEvent {}
|
||||||
|
|
||||||
|
// State
|
||||||
|
enum ExperienceStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class ExperienceState extends Equatable {
|
||||||
|
final ExperienceStatus status;
|
||||||
|
final List<String> selectedIndustries;
|
||||||
|
final List<String> selectedSkills;
|
||||||
|
final List<String> availableIndustries;
|
||||||
|
final List<String> availableSkills;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const ExperienceState({
|
||||||
|
this.status = ExperienceStatus.initial,
|
||||||
|
this.selectedIndustries = const [],
|
||||||
|
this.selectedSkills = const [],
|
||||||
|
this.availableIndustries = const [],
|
||||||
|
this.availableSkills = const [],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ExperienceState copyWith({
|
||||||
|
ExperienceStatus? status,
|
||||||
|
List<String>? selectedIndustries,
|
||||||
|
List<String>? selectedSkills,
|
||||||
|
List<String>? availableIndustries,
|
||||||
|
List<String>? availableSkills,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ExperienceState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
selectedIndustries: selectedIndustries ?? this.selectedIndustries,
|
||||||
|
selectedSkills: selectedSkills ?? this.selectedSkills,
|
||||||
|
availableIndustries: availableIndustries ?? this.availableIndustries,
|
||||||
|
availableSkills: availableSkills ?? this.availableSkills,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
|
selectedIndustries,
|
||||||
|
selectedSkills,
|
||||||
|
availableIndustries,
|
||||||
|
availableSkills,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// BLoC
|
||||||
|
class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
|
||||||
|
static const List<String> _kAllIndustries = [
|
||||||
|
'hospitality',
|
||||||
|
'food_service',
|
||||||
|
'warehouse',
|
||||||
|
'events',
|
||||||
|
'retail',
|
||||||
|
'healthcare',
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<String> _kAllSkills = [
|
||||||
|
'food_service',
|
||||||
|
'bartending',
|
||||||
|
'event_setup',
|
||||||
|
'hospitality',
|
||||||
|
'warehouse',
|
||||||
|
'customer_service',
|
||||||
|
'cleaning',
|
||||||
|
'security',
|
||||||
|
'retail',
|
||||||
|
'cooking',
|
||||||
|
'cashier',
|
||||||
|
'server',
|
||||||
|
'barista',
|
||||||
|
'host_hostess',
|
||||||
|
'busser',
|
||||||
|
];
|
||||||
|
|
||||||
|
final GetStaffIndustriesUseCase getIndustries;
|
||||||
|
final GetStaffSkillsUseCase getSkills;
|
||||||
|
final SaveExperienceUseCase saveExperience;
|
||||||
|
final String staffId;
|
||||||
|
|
||||||
|
ExperienceBloc({
|
||||||
|
required this.getIndustries,
|
||||||
|
required this.getSkills,
|
||||||
|
required this.saveExperience,
|
||||||
|
required this.staffId,
|
||||||
|
}) : super(const ExperienceState(
|
||||||
|
availableIndustries: _kAllIndustries,
|
||||||
|
availableSkills: _kAllSkills,
|
||||||
|
)) {
|
||||||
|
on<ExperienceLoaded>(_onLoaded);
|
||||||
|
on<ExperienceIndustryToggled>(_onIndustryToggled);
|
||||||
|
on<ExperienceSkillToggled>(_onSkillToggled);
|
||||||
|
on<ExperienceCustomSkillAdded>(_onCustomSkillAdded);
|
||||||
|
on<ExperienceSubmitted>(_onSubmitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoaded(
|
||||||
|
ExperienceLoaded event,
|
||||||
|
Emitter<ExperienceState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: ExperienceStatus.loading));
|
||||||
|
try {
|
||||||
|
final arguments = GetExperienceArguments(staffId: staffId);
|
||||||
|
final results = await Future.wait([
|
||||||
|
getIndustries(arguments),
|
||||||
|
getSkills(arguments),
|
||||||
|
]);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: ExperienceStatus.initial,
|
||||||
|
selectedIndustries: results[0],
|
||||||
|
selectedSkills: results[1],
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: ExperienceStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onIndustryToggled(
|
||||||
|
ExperienceIndustryToggled event,
|
||||||
|
Emitter<ExperienceState> emit,
|
||||||
|
) {
|
||||||
|
final industries = List<String>.from(state.selectedIndustries);
|
||||||
|
if (industries.contains(event.industry)) {
|
||||||
|
industries.remove(event.industry);
|
||||||
|
} else {
|
||||||
|
industries.add(event.industry);
|
||||||
|
}
|
||||||
|
emit(state.copyWith(selectedIndustries: industries));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSkillToggled(
|
||||||
|
ExperienceSkillToggled event,
|
||||||
|
Emitter<ExperienceState> emit,
|
||||||
|
) {
|
||||||
|
final skills = List<String>.from(state.selectedSkills);
|
||||||
|
if (skills.contains(event.skill)) {
|
||||||
|
skills.remove(event.skill);
|
||||||
|
} else {
|
||||||
|
skills.add(event.skill);
|
||||||
|
}
|
||||||
|
emit(state.copyWith(selectedSkills: skills));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCustomSkillAdded(
|
||||||
|
ExperienceCustomSkillAdded event,
|
||||||
|
Emitter<ExperienceState> emit,
|
||||||
|
) {
|
||||||
|
if (!state.selectedSkills.contains(event.skill)) {
|
||||||
|
final skills = List<String>.from(state.selectedSkills)..add(event.skill);
|
||||||
|
emit(state.copyWith(selectedSkills: skills));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmitted(
|
||||||
|
ExperienceSubmitted event,
|
||||||
|
Emitter<ExperienceState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: ExperienceStatus.loading));
|
||||||
|
try {
|
||||||
|
await saveExperience(
|
||||||
|
SaveExperienceArguments(
|
||||||
|
staffId: staffId,
|
||||||
|
industries: state.selectedIndustries,
|
||||||
|
skills: state.selectedSkills,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(state.copyWith(status: ExperienceStatus.success));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: ExperienceStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
extension ExperienceNavigator on IModularNavigator {
|
||||||
|
// Add navigation methods here if the page navigates deeper.
|
||||||
|
// Currently ExperiencePage is a leaf, but might need to navigate back or to success screen.
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import '../blocs/experience_bloc.dart';
|
||||||
|
import '../widgets/experience_badge.dart';
|
||||||
|
import '../widgets/experience_custom_input.dart';
|
||||||
|
import '../widgets/experience_section_title.dart';
|
||||||
|
|
||||||
|
class ExperiencePage extends StatelessWidget {
|
||||||
|
const ExperiencePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => Modular.get<ExperienceBloc>()..add(ExperienceLoaded()),
|
||||||
|
child: const _ExperienceView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExperienceView extends StatelessWidget {
|
||||||
|
const _ExperienceView();
|
||||||
|
|
||||||
|
String _getIndustryLabel(dynamic node, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'hospitality': return node.hospitality;
|
||||||
|
case 'food_service': return node.food_service;
|
||||||
|
case 'warehouse': return node.warehouse;
|
||||||
|
case 'events': return node.events;
|
||||||
|
case 'retail': return node.retail;
|
||||||
|
case 'healthcare': return node.healthcare;
|
||||||
|
case 'other': return node.other;
|
||||||
|
default: return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSkillLabel(dynamic node, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'food_service': return node.food_service;
|
||||||
|
case 'bartending': return node.bartending;
|
||||||
|
case 'event_setup': return node.event_setup;
|
||||||
|
case 'hospitality': return node.hospitality;
|
||||||
|
case 'warehouse': return node.warehouse;
|
||||||
|
case 'customer_service': return node.customer_service;
|
||||||
|
case 'cleaning': return node.cleaning;
|
||||||
|
case 'security': return node.security;
|
||||||
|
case 'retail': return node.retail;
|
||||||
|
case 'cooking': return node.cooking;
|
||||||
|
case 'cashier': return node.cashier;
|
||||||
|
case 'server': return node.server;
|
||||||
|
case 'barista': return node.barista;
|
||||||
|
case 'host_hostess': return node.host_hostess;
|
||||||
|
case 'busser': return node.busser;
|
||||||
|
default: return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final i18n = t.staff.onboarding.experience;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: UiColors.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: UiColors.bgPopup,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||||
|
onPressed: () => Modular.to.pop(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
i18n.title,
|
||||||
|
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1.0),
|
||||||
|
child: Container(color: UiColors.border, height: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: BlocConsumer<ExperienceBloc, ExperienceState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.status == ExperienceStatus.success) {
|
||||||
|
Modular.to.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ExperienceSectionTitle(title: i18n.industries_title),
|
||||||
|
Text(
|
||||||
|
i18n.industries_subtitle,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space3),
|
||||||
|
Wrap(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
runSpacing: UiConstants.space2,
|
||||||
|
children: state.availableIndustries
|
||||||
|
.map(
|
||||||
|
(i) => ExperienceBadge(
|
||||||
|
label: _getIndustryLabel(i18n.industries, i),
|
||||||
|
isSelected: state.selectedIndustries.contains(i),
|
||||||
|
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||||
|
.add(ExperienceIndustryToggled(i)),
|
||||||
|
isIndustry: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space6),
|
||||||
|
ExperienceSectionTitle(title: i18n.skills_title),
|
||||||
|
Text(
|
||||||
|
i18n.skills_subtitle,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space3),
|
||||||
|
Wrap(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
runSpacing: UiConstants.space2,
|
||||||
|
children: state.availableSkills
|
||||||
|
.map(
|
||||||
|
(s) => ExperienceBadge(
|
||||||
|
label: _getSkillLabel(i18n.skills, s),
|
||||||
|
isSelected: state.selectedSkills.contains(s),
|
||||||
|
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||||
|
.add(ExperienceSkillToggled(s)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space4),
|
||||||
|
const ExperienceCustomInput(),
|
||||||
|
SizedBox(height: UiConstants.space4),
|
||||||
|
_buildCustomSkillsList(state, i18n),
|
||||||
|
SizedBox(height: UiConstants.space10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildSaveButton(context, state, i18n),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomSkillsList(ExperienceState state, dynamic i18n) {
|
||||||
|
final customSkills = state.selectedSkills
|
||||||
|
.where((s) => !state.availableSkills.contains(s))
|
||||||
|
.toList();
|
||||||
|
if (customSkills.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
i18n.custom_skills_title,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
SizedBox(height: UiConstants.space2),
|
||||||
|
Wrap(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
runSpacing: UiConstants.space2,
|
||||||
|
children: customSkills.map((skill) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
vertical: UiConstants.space2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
skill,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton(BuildContext context, ExperienceState state, dynamic i18n) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: state.status == ExperienceStatus.loading
|
||||||
|
? null
|
||||||
|
: () => BlocProvider.of<ExperienceBloc>(context).add(ExperienceSubmitted()),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.primaryForeground,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: state.status == ExperienceStatus.loading
|
||||||
|
? SizedBox(
|
||||||
|
height: 20.0,
|
||||||
|
width: 20.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(UiColors.primaryForeground),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
i18n.save_button,
|
||||||
|
style: UiTypography.title2b,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ExperienceBadge extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool isIndustry;
|
||||||
|
|
||||||
|
const ExperienceBadge({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
this.isIndustry = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
vertical: UiConstants.space2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? UiColors.primary : Colors.transparent,
|
||||||
|
borderRadius: UiConstants.radiusFull,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: isSelected ? UiColors.primaryForeground : UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../blocs/experience_bloc.dart';
|
||||||
|
|
||||||
|
class ExperienceCustomInput extends StatefulWidget {
|
||||||
|
const ExperienceCustomInput({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExperienceCustomInput> createState() => _ExperienceCustomInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExperienceCustomInputState extends State<ExperienceCustomInput> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
void _addSkill() {
|
||||||
|
final skill = _controller.text.trim();
|
||||||
|
if (skill.isNotEmpty) {
|
||||||
|
BlocProvider.of<ExperienceBloc>(context).add(ExperienceCustomSkillAdded(skill));
|
||||||
|
_controller.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
onSubmitted: (_) => _addSkill(),
|
||||||
|
style: UiTypography.body1r.copyWith(color: UiColors.textPrimary),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: t.staff.onboarding.experience.custom_skill_hint,
|
||||||
|
hintStyle: UiTypography.body1r.copyWith(color: UiColors.textPlaceholder),
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
vertical: UiConstants.space3,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
borderSide: BorderSide(color: UiColors.border),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
borderSide: BorderSide(color: UiColors.border),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
borderSide: BorderSide(color: UiColors.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fillColor: UiColors.bgPopup,
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: UiConstants.space2),
|
||||||
|
InkWell(
|
||||||
|
onTap: _addSkill,
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusMd),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(UiIcons.add, color: UiColors.primaryForeground),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ExperienceSectionTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
const ExperienceSectionTitle({super.key, required this.title});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.title2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
library staff_profile_experience;
|
||||||
|
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
|
import 'src/data/repositories/experience_repository_impl.dart';
|
||||||
|
import 'src/domain/repositories/experience_repository_interface.dart';
|
||||||
|
import 'src/domain/usecases/get_staff_industries_usecase.dart';
|
||||||
|
import 'src/domain/usecases/get_staff_skills_usecase.dart';
|
||||||
|
import 'src/domain/usecases/save_experience_usecase.dart';
|
||||||
|
import 'src/presentation/blocs/experience_bloc.dart';
|
||||||
|
import 'src/presentation/pages/experience_page.dart';
|
||||||
|
|
||||||
|
export 'src/presentation/pages/experience_page.dart';
|
||||||
|
|
||||||
|
class StaffProfileExperienceModule extends Module {
|
||||||
|
@override
|
||||||
|
List<Module> get imports => [DataConnectModule()];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void binds(Injector i) {
|
||||||
|
// Repository
|
||||||
|
i.addLazySingleton<ExperienceRepositoryInterface>(
|
||||||
|
() => ExperienceRepositoryImpl(i.get<ProfileRepositoryMock>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// UseCases
|
||||||
|
i.addLazySingleton<GetStaffIndustriesUseCase>(
|
||||||
|
() => GetStaffIndustriesUseCase(i.get<ExperienceRepositoryInterface>()),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<GetStaffSkillsUseCase>(
|
||||||
|
() => GetStaffSkillsUseCase(i.get<ExperienceRepositoryInterface>()),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<SaveExperienceUseCase>(
|
||||||
|
() => SaveExperienceUseCase(i.get<ExperienceRepositoryInterface>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// BLoC
|
||||||
|
i.addLazySingleton<ExperienceBloc>(
|
||||||
|
() => ExperienceBloc(
|
||||||
|
getIndustries: i.get<GetStaffIndustriesUseCase>(),
|
||||||
|
getSkills: i.get<GetStaffSkillsUseCase>(),
|
||||||
|
saveExperience: i.get<SaveExperienceUseCase>(),
|
||||||
|
// TODO: Get actual logged in staff ID
|
||||||
|
staffId: 'current-staff-id',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routes(RouteManager r) {
|
||||||
|
r.child(
|
||||||
|
'/',
|
||||||
|
child: (_) => const ExperiencePage(),
|
||||||
|
transition: TransitionType.rightToLeft,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: staff_profile_experience
|
||||||
|
description: Staff Profile Experience feature.
|
||||||
|
version: 0.0.1
|
||||||
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_bloc: ^8.1.0
|
||||||
|
flutter_modular: ^6.3.0
|
||||||
|
equatable: ^2.0.5
|
||||||
|
|
||||||
|
# Architecture Packages
|
||||||
|
krow_domain:
|
||||||
|
path: ../../../../../../domain
|
||||||
|
krow_core:
|
||||||
|
path: ../../../../../../core
|
||||||
|
krow_data_connect:
|
||||||
|
path: ../../../../../../data_connect
|
||||||
|
design_system:
|
||||||
|
path: ../../../../../../design_system
|
||||||
|
core_localization:
|
||||||
|
path: ../../../../../../core_localization
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
bloc_test: ^9.1.0
|
||||||
|
mocktail: ^1.0.0
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
@@ -14,6 +14,7 @@ workspace:
|
|||||||
- packages/features/staff/staff_main
|
- packages/features/staff/staff_main
|
||||||
- packages/features/staff/profile
|
- packages/features/staff/profile
|
||||||
- packages/features/staff/profile_sections/onboarding/emergency_contact
|
- packages/features/staff/profile_sections/onboarding/emergency_contact
|
||||||
|
- packages/features/staff/profile_sections/onboarding/experience
|
||||||
- packages/features/staff/profile_sections/onboarding/profile_info
|
- packages/features/staff/profile_sections/onboarding/profile_info
|
||||||
- packages/features/client/authentication
|
- packages/features/client/authentication
|
||||||
- packages/features/client/home
|
- packages/features/client/home
|
||||||
|
|||||||
Reference in New Issue
Block a user