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...",
|
||||
"save_button": "Save Changes",
|
||||
"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/home_repository_mock.dart';
|
||||
import 'mocks/order_repository_mock.dart';
|
||||
import 'mocks/profile_repository_mock.dart';
|
||||
|
||||
/// A module that provides Data Connect dependencies, including mocks.
|
||||
class DataConnectModule extends Module {
|
||||
@@ -10,6 +11,7 @@ class DataConnectModule extends Module {
|
||||
void exportedBinds(Injector i) {
|
||||
// Make these mocks available to any module that imports this one.
|
||||
i.addLazySingleton(AuthRepositoryMock.new);
|
||||
i.addLazySingleton(ProfileRepositoryMock.new);
|
||||
i.addLazySingleton(HomeRepositoryMock.new);
|
||||
i.addLazySingleton(BusinessRepositoryMock.new);
|
||||
i.addLazySingleton(OrderRepositoryMock.new);
|
||||
|
||||
@@ -65,4 +65,26 @@ class ProfileRepositoryMock {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
// 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.
|
||||
void pushExperience() {
|
||||
pushNamed('/profile/onboarding/experience');
|
||||
pushNamed('/profile/experience');
|
||||
}
|
||||
|
||||
/// 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:staff_profile_info/staff_profile_info.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 'domain/repositories/profile_repository.dart';
|
||||
@@ -55,5 +56,6 @@ class StaffProfileModule extends Module {
|
||||
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||
r.module('/experience', module: StaffProfileExperienceModule());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ dependencies:
|
||||
path: ../profile_sections/onboarding/profile_info
|
||||
staff_emergency_contact:
|
||||
path: ../profile_sections/onboarding/emergency_contact
|
||||
staff_profile_experience:
|
||||
path: ../profile_sections/onboarding/experience
|
||||
|
||||
dev_dependencies:
|
||||
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/profile
|
||||
- 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/client/authentication
|
||||
- packages/features/client/home
|
||||
|
||||
Reference in New Issue
Block a user