feat: migrate experience management to V2 API; add support for industries and skills
This commit is contained in:
@@ -75,6 +75,10 @@ abstract final class StaffEndpoints {
|
||||
/// Skills.
|
||||
static const ApiEndpoint skills = ApiEndpoint('/staff/profile/skills');
|
||||
|
||||
/// Save/update experience (industries + skills).
|
||||
static const ApiEndpoint experience =
|
||||
ApiEndpoint('/staff/profile/experience');
|
||||
|
||||
/// Documents.
|
||||
static const ApiEndpoint documents =
|
||||
ApiEndpoint('/staff/profile/documents');
|
||||
|
||||
@@ -823,6 +823,8 @@
|
||||
"custom_skills_title": "Custom Skills:",
|
||||
"custom_skill_hint": "Add custom skill...",
|
||||
"save_button": "Save & Continue",
|
||||
"save_success": "Experience saved successfully",
|
||||
"save_error": "An error occurred",
|
||||
"industries": {
|
||||
"hospitality": "Hospitality",
|
||||
"food_service": "Food Service",
|
||||
@@ -830,6 +832,8 @@
|
||||
"events": "Events",
|
||||
"retail": "Retail",
|
||||
"healthcare": "Healthcare",
|
||||
"catering": "Catering",
|
||||
"cafe": "Cafe",
|
||||
"other": "Other"
|
||||
},
|
||||
"skills": {
|
||||
|
||||
@@ -818,6 +818,8 @@
|
||||
"custom_skills_title": "Habilidades personalizadas:",
|
||||
"custom_skill_hint": "A\u00f1adir habilidad...",
|
||||
"save_button": "Guardar y continuar",
|
||||
"save_success": "Experiencia guardada exitosamente",
|
||||
"save_error": "Ocurrió un error",
|
||||
"industries": {
|
||||
"hospitality": "Hoteler\u00eda",
|
||||
"food_service": "Servicio de alimentos",
|
||||
@@ -825,6 +827,8 @@
|
||||
"events": "Eventos",
|
||||
"retail": "Venta al por menor",
|
||||
"healthcare": "Cuidado de la salud",
|
||||
"catering": "Catering",
|
||||
"cafe": "Cafetería",
|
||||
"other": "Otro"
|
||||
},
|
||||
"skills": {
|
||||
|
||||
@@ -19,6 +19,8 @@ export 'src/entities/enums/onboarding_status.dart';
|
||||
export 'src/entities/enums/order_type.dart';
|
||||
export 'src/entities/enums/payment_status.dart';
|
||||
export 'src/entities/enums/shift_status.dart';
|
||||
export 'src/entities/enums/staff_industry.dart';
|
||||
export 'src/entities/enums/staff_skill.dart';
|
||||
export 'src/entities/enums/staff_status.dart';
|
||||
export 'src/entities/enums/user_role.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/// Industry options for staff experience profiles.
|
||||
///
|
||||
/// Values match the V2 API format (UPPER_SNAKE_CASE).
|
||||
enum StaffIndustry {
|
||||
/// Hospitality industry.
|
||||
hospitality('HOSPITALITY'),
|
||||
|
||||
/// Food service industry.
|
||||
foodService('FOOD_SERVICE'),
|
||||
|
||||
/// Warehouse / logistics industry.
|
||||
warehouse('WAREHOUSE'),
|
||||
|
||||
/// Events industry.
|
||||
events('EVENTS'),
|
||||
|
||||
/// Retail industry.
|
||||
retail('RETAIL'),
|
||||
|
||||
/// Healthcare industry.
|
||||
healthcare('HEALTHCARE'),
|
||||
|
||||
/// Catering industry.
|
||||
catering('CATERING'),
|
||||
|
||||
/// Cafe / coffee shop industry.
|
||||
cafe('CAFE'),
|
||||
|
||||
/// Other / unspecified industry.
|
||||
other('OTHER');
|
||||
|
||||
const StaffIndustry(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static StaffIndustry? fromJson(String? value) {
|
||||
if (value == null) return null;
|
||||
for (final StaffIndustry industry in StaffIndustry.values) {
|
||||
if (industry.value == value) return industry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/// Skill options for staff experience profiles.
|
||||
///
|
||||
/// Values match the V2 API format (UPPER_SNAKE_CASE).
|
||||
enum StaffSkill {
|
||||
/// Food service skill.
|
||||
foodService('FOOD_SERVICE'),
|
||||
|
||||
/// Bartending skill.
|
||||
bartending('BARTENDING'),
|
||||
|
||||
/// Event setup skill.
|
||||
eventSetup('EVENT_SETUP'),
|
||||
|
||||
/// Hospitality skill.
|
||||
hospitality('HOSPITALITY'),
|
||||
|
||||
/// Warehouse skill.
|
||||
warehouse('WAREHOUSE'),
|
||||
|
||||
/// Customer service skill.
|
||||
customerService('CUSTOMER_SERVICE'),
|
||||
|
||||
/// Cleaning skill.
|
||||
cleaning('CLEANING'),
|
||||
|
||||
/// Security skill.
|
||||
security('SECURITY'),
|
||||
|
||||
/// Retail skill.
|
||||
retail('RETAIL'),
|
||||
|
||||
/// Driving skill.
|
||||
driving('DRIVING'),
|
||||
|
||||
/// Cooking skill.
|
||||
cooking('COOKING'),
|
||||
|
||||
/// Cashier skill.
|
||||
cashier('CASHIER'),
|
||||
|
||||
/// Server skill.
|
||||
server('SERVER'),
|
||||
|
||||
/// Barista skill.
|
||||
barista('BARISTA'),
|
||||
|
||||
/// Host / hostess skill.
|
||||
hostHostess('HOST_HOSTESS'),
|
||||
|
||||
/// Busser skill.
|
||||
busser('BUSSER');
|
||||
|
||||
const StaffSkill(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static StaffSkill? fromJson(String? value) {
|
||||
if (value == null) return null;
|
||||
for (final StaffSkill skill in StaffSkill.values) {
|
||||
if (skill.value == value) return skill;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_profile_experience/src/domain/repositories/experience_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ExperienceRepositoryInterface] using the V2 API.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||
/// Creates an [ExperienceRepositoryImpl].
|
||||
ExperienceRepositoryImpl({required BaseApiService apiService})
|
||||
@@ -14,30 +12,54 @@ class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<String>> getIndustries() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(StaffEndpoints.industries);
|
||||
final List<dynamic> items = response.data['industries'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items.map((dynamic e) => e.toString()).toList();
|
||||
Future<List<StaffIndustry>> getIndustries() async {
|
||||
final ApiResponse response = await _api.get(StaffEndpoints.industries);
|
||||
final List<dynamic> items =
|
||||
response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items
|
||||
.map((dynamic e) => StaffIndustry.fromJson(e.toString()))
|
||||
.whereType<StaffIndustry>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> getSkills() async {
|
||||
Future<({List<StaffSkill> skills, List<String> customSkills})>
|
||||
getSkills() async {
|
||||
final ApiResponse response = await _api.get(StaffEndpoints.skills);
|
||||
final List<dynamic> items = response.data['skills'] as List<dynamic>? ?? <dynamic>[];
|
||||
return items.map((dynamic e) => e.toString()).toList();
|
||||
final List<dynamic> items =
|
||||
response.data['items'] as List<dynamic>? ?? <dynamic>[];
|
||||
|
||||
final List<StaffSkill> skills = <StaffSkill>[];
|
||||
final List<String> customSkills = <String>[];
|
||||
|
||||
for (final dynamic item in items) {
|
||||
final String value = item.toString();
|
||||
final StaffSkill? parsed = StaffSkill.fromJson(value);
|
||||
if (parsed != null) {
|
||||
skills.add(parsed);
|
||||
} else {
|
||||
customSkills.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (skills: skills, customSkills: customSkills);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveExperience(
|
||||
List<String> industries,
|
||||
List<String> skills,
|
||||
List<StaffIndustry> industries,
|
||||
List<StaffSkill> skills,
|
||||
List<String> customSkills,
|
||||
) async {
|
||||
await _api.put(
|
||||
StaffEndpoints.personalInfo,
|
||||
StaffEndpoints.experience,
|
||||
data: <String, dynamic>{
|
||||
'industries': industries,
|
||||
'skills': skills,
|
||||
'industries':
|
||||
industries.map((StaffIndustry i) => i.value).toList(),
|
||||
'skills': <String>[
|
||||
...skills.map((StaffSkill s) => s.value),
|
||||
...customSkills,
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
/// Arguments for the [SaveExperienceUseCase].
|
||||
class SaveExperienceArguments extends UseCaseArgument {
|
||||
final List<String> industries;
|
||||
final List<String> skills;
|
||||
|
||||
/// Creates a [SaveExperienceArguments].
|
||||
const SaveExperienceArguments({
|
||||
required this.industries,
|
||||
required this.skills,
|
||||
this.customSkills = const <String>[],
|
||||
});
|
||||
|
||||
/// Selected industries.
|
||||
final List<StaffIndustry> industries;
|
||||
|
||||
/// Selected predefined skills.
|
||||
final List<StaffSkill> skills;
|
||||
|
||||
/// Custom skills not in the [StaffSkill] enum.
|
||||
final List<String> customSkills;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [industries, skills];
|
||||
List<Object?> get props => <Object?>[industries, skills, customSkills];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
/// Interface for accessing staff experience data.
|
||||
abstract class ExperienceRepositoryInterface {
|
||||
/// Fetches the list of industries associated with the staff member.
|
||||
Future<List<String>> getIndustries();
|
||||
Future<List<StaffIndustry>> getIndustries();
|
||||
|
||||
/// Fetches the list of skills associated with the staff member.
|
||||
Future<List<String>> getSkills();
|
||||
///
|
||||
/// Returns recognised [StaffSkill] values. Unrecognised API values are
|
||||
/// returned in [customSkills].
|
||||
Future<({List<StaffSkill> skills, List<String> customSkills})> getSkills();
|
||||
|
||||
/// Saves the staff member's experience (industries and skills).
|
||||
Future<void> saveExperience(
|
||||
List<String> industries,
|
||||
List<String> skills,
|
||||
List<StaffIndustry> industries,
|
||||
List<StaffSkill> skills,
|
||||
List<String> customSkills,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry;
|
||||
|
||||
import '../repositories/experience_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching staff industries.
|
||||
class GetStaffIndustriesUseCase implements NoInputUseCase<List<String>> {
|
||||
final ExperienceRepositoryInterface _repository;
|
||||
|
||||
class GetStaffIndustriesUseCase
|
||||
implements NoInputUseCase<List<StaffIndustry>> {
|
||||
/// Creates a [GetStaffIndustriesUseCase].
|
||||
GetStaffIndustriesUseCase(this._repository);
|
||||
|
||||
final ExperienceRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<String>> call() {
|
||||
Future<List<StaffIndustry>> call() {
|
||||
return _repository.getIndustries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffSkill;
|
||||
|
||||
import '../repositories/experience_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching staff skills.
|
||||
class GetStaffSkillsUseCase implements NoInputUseCase<List<String>> {
|
||||
final ExperienceRepositoryInterface _repository;
|
||||
|
||||
class GetStaffSkillsUseCase
|
||||
implements
|
||||
NoInputUseCase<({List<StaffSkill> skills, List<String> customSkills})> {
|
||||
/// Creates a [GetStaffSkillsUseCase].
|
||||
GetStaffSkillsUseCase(this._repository);
|
||||
|
||||
final ExperienceRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<String>> call() {
|
||||
Future<({List<StaffSkill> skills, List<String> customSkills})> call() {
|
||||
return _repository.getSkills();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +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);
|
||||
|
||||
/// The experience repository.
|
||||
final ExperienceRepositoryInterface repository;
|
||||
|
||||
@override
|
||||
Future<void> call(SaveExperienceArguments params) {
|
||||
return repository.saveExperience(
|
||||
params.industries,
|
||||
params.skills,
|
||||
params.customSkills,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,144 +1,26 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
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';
|
||||
import 'experience_event.dart';
|
||||
import 'experience_state.dart';
|
||||
|
||||
// Events
|
||||
abstract class ExperienceEvent extends Equatable {
|
||||
const ExperienceEvent();
|
||||
export 'experience_event.dart';
|
||||
export 'experience_state.dart';
|
||||
|
||||
@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,
|
||||
];
|
||||
}
|
||||
|
||||
/// Available industry option values.
|
||||
const List<String> _kAvailableIndustries = <String>[
|
||||
'hospitality',
|
||||
'food_service',
|
||||
'warehouse',
|
||||
'events',
|
||||
'retail',
|
||||
'healthcare',
|
||||
'other',
|
||||
];
|
||||
|
||||
/// Available skill option values.
|
||||
const List<String> _kAvailableSkills = <String>[
|
||||
'food_service',
|
||||
'bartending',
|
||||
'event_setup',
|
||||
'hospitality',
|
||||
'warehouse',
|
||||
'customer_service',
|
||||
'cleaning',
|
||||
'security',
|
||||
'retail',
|
||||
'driving',
|
||||
'cooking',
|
||||
'cashier',
|
||||
'server',
|
||||
'barista',
|
||||
'host_hostess',
|
||||
'busser',
|
||||
];
|
||||
|
||||
// BLoC
|
||||
/// BLoC that manages the staff experience (industries & skills) selection.
|
||||
class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
with BlocErrorHandler<ExperienceState> {
|
||||
final GetStaffIndustriesUseCase getIndustries;
|
||||
final GetStaffSkillsUseCase getSkills;
|
||||
final SaveExperienceUseCase saveExperience;
|
||||
|
||||
/// Creates an [ExperienceBloc].
|
||||
ExperienceBloc({
|
||||
required this.getIndustries,
|
||||
required this.getSkills,
|
||||
required this.saveExperience,
|
||||
}) : super(
|
||||
const ExperienceState(
|
||||
availableIndustries: _kAvailableIndustries,
|
||||
availableSkills: _kAvailableSkills,
|
||||
),
|
||||
) {
|
||||
}) : super(const ExperienceState()) {
|
||||
on<ExperienceLoaded>(_onLoaded);
|
||||
on<ExperienceIndustryToggled>(_onIndustryToggled);
|
||||
on<ExperienceSkillToggled>(_onSkillToggled);
|
||||
@@ -148,6 +30,15 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
add(ExperienceLoaded());
|
||||
}
|
||||
|
||||
/// Use case for fetching saved industries.
|
||||
final GetStaffIndustriesUseCase getIndustries;
|
||||
|
||||
/// Use case for fetching saved skills.
|
||||
final GetStaffSkillsUseCase getSkills;
|
||||
|
||||
/// Use case for saving experience selections.
|
||||
final SaveExperienceUseCase saveExperience;
|
||||
|
||||
Future<void> _onLoaded(
|
||||
ExperienceLoaded event,
|
||||
Emitter<ExperienceState> emit,
|
||||
@@ -156,13 +47,16 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final results = await Future.wait([getIndustries(), getSkills()]);
|
||||
final List<StaffIndustry> industries = await getIndustries();
|
||||
final ({List<StaffSkill> skills, List<String> customSkills}) skillsResult =
|
||||
await getSkills();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ExperienceStatus.initial,
|
||||
selectedIndustries: results[0],
|
||||
selectedSkills: results[1],
|
||||
selectedIndustries: industries,
|
||||
selectedSkills: skillsResult.skills,
|
||||
customSkills: skillsResult.customSkills,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -177,7 +71,8 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
ExperienceIndustryToggled event,
|
||||
Emitter<ExperienceState> emit,
|
||||
) {
|
||||
final industries = List<String>.from(state.selectedIndustries);
|
||||
final List<StaffIndustry> industries =
|
||||
List<StaffIndustry>.from(state.selectedIndustries);
|
||||
if (industries.contains(event.industry)) {
|
||||
industries.remove(event.industry);
|
||||
} else {
|
||||
@@ -190,7 +85,8 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
ExperienceSkillToggled event,
|
||||
Emitter<ExperienceState> emit,
|
||||
) {
|
||||
final skills = List<String>.from(state.selectedSkills);
|
||||
final List<StaffSkill> skills =
|
||||
List<StaffSkill>.from(state.selectedSkills);
|
||||
if (skills.contains(event.skill)) {
|
||||
skills.remove(event.skill);
|
||||
} else {
|
||||
@@ -203,9 +99,10 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
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));
|
||||
if (!state.customSkills.contains(event.skill)) {
|
||||
final List<String> custom = List<String>.from(state.customSkills)
|
||||
..add(event.skill);
|
||||
emit(state.copyWith(customSkills: custom));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +118,7 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
|
||||
SaveExperienceArguments(
|
||||
industries: state.selectedIndustries,
|
||||
skills: state.selectedSkills,
|
||||
customSkills: state.customSkills,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(status: ExperienceStatus.success));
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
/// Base event for the experience BLoC.
|
||||
abstract class ExperienceEvent extends Equatable {
|
||||
/// Creates an [ExperienceEvent].
|
||||
const ExperienceEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Triggers initial load of saved industries and skills.
|
||||
class ExperienceLoaded extends ExperienceEvent {}
|
||||
|
||||
/// Toggles an industry selection on or off.
|
||||
class ExperienceIndustryToggled extends ExperienceEvent {
|
||||
/// Creates an [ExperienceIndustryToggled] event.
|
||||
const ExperienceIndustryToggled(this.industry);
|
||||
|
||||
/// The industry to toggle.
|
||||
final StaffIndustry industry;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[industry];
|
||||
}
|
||||
|
||||
/// Toggles a skill selection on or off.
|
||||
class ExperienceSkillToggled extends ExperienceEvent {
|
||||
/// Creates an [ExperienceSkillToggled] event.
|
||||
const ExperienceSkillToggled(this.skill);
|
||||
|
||||
/// The skill to toggle.
|
||||
final StaffSkill skill;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[skill];
|
||||
}
|
||||
|
||||
/// Adds a custom skill not in the predefined [StaffSkill] enum.
|
||||
class ExperienceCustomSkillAdded extends ExperienceEvent {
|
||||
/// Creates an [ExperienceCustomSkillAdded] event.
|
||||
const ExperienceCustomSkillAdded(this.skill);
|
||||
|
||||
/// The custom skill value to add.
|
||||
final String skill;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[skill];
|
||||
}
|
||||
|
||||
/// Submits the selected industries and skills to the backend.
|
||||
class ExperienceSubmitted extends ExperienceEvent {}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
/// Status of the experience feature.
|
||||
enum ExperienceStatus {
|
||||
/// Initial state before any action.
|
||||
initial,
|
||||
|
||||
/// Loading data from the backend.
|
||||
loading,
|
||||
|
||||
/// Operation completed successfully.
|
||||
success,
|
||||
|
||||
/// An error occurred.
|
||||
failure,
|
||||
}
|
||||
|
||||
/// State for the experience BLoC.
|
||||
class ExperienceState extends Equatable {
|
||||
/// Creates an [ExperienceState].
|
||||
const ExperienceState({
|
||||
this.status = ExperienceStatus.initial,
|
||||
this.selectedIndustries = const <StaffIndustry>[],
|
||||
this.selectedSkills = const <StaffSkill>[],
|
||||
this.customSkills = const <String>[],
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Current operation status.
|
||||
final ExperienceStatus status;
|
||||
|
||||
/// Industries the staff member has selected.
|
||||
final List<StaffIndustry> selectedIndustries;
|
||||
|
||||
/// Skills the staff member has selected.
|
||||
final List<StaffSkill> selectedSkills;
|
||||
|
||||
/// Custom skills not in [StaffSkill] that the user added.
|
||||
final List<String> customSkills;
|
||||
|
||||
/// Error message key when [status] is [ExperienceStatus.failure].
|
||||
final String? errorMessage;
|
||||
|
||||
/// All selected skill values as API strings (enum + custom combined).
|
||||
List<String> get allSkillValues =>
|
||||
<String>[
|
||||
...selectedSkills.map((StaffSkill s) => s.value),
|
||||
...customSkills,
|
||||
];
|
||||
|
||||
/// Creates a copy with the given fields replaced.
|
||||
ExperienceState copyWith({
|
||||
ExperienceStatus? status,
|
||||
List<StaffIndustry>? selectedIndustries,
|
||||
List<StaffSkill>? selectedSkills,
|
||||
List<String>? customSkills,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return ExperienceState(
|
||||
status: status ?? this.status,
|
||||
selectedIndustries: selectedIndustries ?? this.selectedIndustries,
|
||||
selectedSkills: selectedSkills ?? this.selectedSkills,
|
||||
customSkills: customSkills ?? this.customSkills,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
selectedIndustries,
|
||||
selectedSkills,
|
||||
customSkills,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -4,90 +4,33 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' show StaffIndustry, StaffSkill;
|
||||
|
||||
import '../blocs/experience_bloc.dart';
|
||||
import '../widgets/experience_section_title.dart';
|
||||
|
||||
/// Page for selecting staff industries and skills.
|
||||
class ExperiencePage extends StatelessWidget {
|
||||
/// Creates an [ExperiencePage].
|
||||
const ExperiencePage({super.key});
|
||||
|
||||
String _getIndustryLabel(dynamic node, String industry) {
|
||||
switch (industry) {
|
||||
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 industry;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSkillLabel(dynamic node, String skill) {
|
||||
switch (skill) {
|
||||
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 'driving':
|
||||
return node.driving;
|
||||
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 skill;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.onboarding.experience;
|
||||
final dynamic i18n = Translations.of(context).staff.onboarding.experience;
|
||||
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: i18n.title,
|
||||
title: i18n.title as String,
|
||||
onLeadingPressed: () => Modular.to.toProfile(),
|
||||
),
|
||||
body: BlocProvider<ExperienceBloc>(
|
||||
create: (context) => Modular.get<ExperienceBloc>(),
|
||||
create: (BuildContext context) => Modular.get<ExperienceBloc>(),
|
||||
child: BlocConsumer<ExperienceBloc, ExperienceState>(
|
||||
listener: (context, state) {
|
||||
listener: (BuildContext context, ExperienceState state) {
|
||||
if (state.status == ExperienceStatus.success) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Experience saved successfully',
|
||||
message: i18n.save_success as String,
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 120,
|
||||
@@ -100,7 +43,7 @@ class ExperiencePage extends StatelessWidget {
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
: i18n.save_error as String,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 120,
|
||||
@@ -110,65 +53,28 @@ class ExperiencePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
builder: (BuildContext context, ExperienceState state) {
|
||||
return Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
ExperienceSectionTitle(
|
||||
title: i18n.industries_title,
|
||||
subtitle: i18n.industries_subtitle,
|
||||
title: i18n.industries_title as String,
|
||||
subtitle: i18n.industries_subtitle as String,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: state.availableIndustries
|
||||
.map(
|
||||
(i) => UiChip(
|
||||
label: _getIndustryLabel(i18n.industries, i),
|
||||
isSelected: state.selectedIndustries.contains(
|
||||
i,
|
||||
),
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(
|
||||
context,
|
||||
).add(ExperienceIndustryToggled(i)),
|
||||
variant: state.selectedIndustries.contains(i)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
_buildIndustryChips(context, state, i18n),
|
||||
const SizedBox(height: UiConstants.space10),
|
||||
ExperienceSectionTitle(
|
||||
title: i18n.skills_title,
|
||||
subtitle: i18n.skills_subtitle,
|
||||
title: i18n.skills_title as String,
|
||||
subtitle: i18n.skills_subtitle as String,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: state.availableSkills
|
||||
.map(
|
||||
(s) => UiChip(
|
||||
label: _getSkillLabel(i18n.skills, s),
|
||||
isSelected: state.selectedSkills.contains(s),
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(
|
||||
context,
|
||||
).add(ExperienceSkillToggled(s)),
|
||||
variant:
|
||||
state.selectedSkills.contains(s)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
_buildSkillChips(context, state, i18n),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -182,28 +88,45 @@ class ExperiencePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomSkillsList(ExperienceState state, dynamic i18n) {
|
||||
final customSkills = state.selectedSkills
|
||||
.where((s) => !state.availableSkills.contains(s))
|
||||
.toList();
|
||||
if (customSkills.isEmpty) return const SizedBox.shrink();
|
||||
Widget _buildIndustryChips(
|
||||
BuildContext context,
|
||||
ExperienceState state,
|
||||
dynamic i18n,
|
||||
) {
|
||||
return Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: StaffIndustry.values.map((StaffIndustry industry) {
|
||||
final bool isSelected = state.selectedIndustries.contains(industry);
|
||||
return UiChip(
|
||||
label: _getIndustryLabel(i18n.industries, industry),
|
||||
isSelected: isSelected,
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceIndustryToggled(industry)),
|
||||
variant: isSelected ? UiChipVariant.primary : UiChipVariant.secondary,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.custom_skills_title,
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: customSkills.map((skill) {
|
||||
return UiChip(label: skill, variant: UiChipVariant.accent);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
Widget _buildSkillChips(
|
||||
BuildContext context,
|
||||
ExperienceState state,
|
||||
dynamic i18n,
|
||||
) {
|
||||
return Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
children: StaffSkill.values.map((StaffSkill skill) {
|
||||
final bool isSelected = state.selectedSkills.contains(skill);
|
||||
return UiChip(
|
||||
label: _getSkillLabel(i18n.skills, skill),
|
||||
isSelected: isSelected,
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceSkillToggled(skill)),
|
||||
variant: isSelected ? UiChipVariant.primary : UiChipVariant.secondary,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,6 +135,7 @@ class ExperiencePage extends StatelessWidget {
|
||||
ExperienceState state,
|
||||
dynamic i18n,
|
||||
) {
|
||||
final bool isLoading = state.status == ExperienceStatus.loading;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -220,24 +144,20 @@ class ExperiencePage extends StatelessWidget {
|
||||
),
|
||||
child: SafeArea(
|
||||
child: UiButton.primary(
|
||||
onPressed: state.status == ExperienceStatus.loading
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () => BlocProvider.of<ExperienceBloc>(
|
||||
context,
|
||||
).add(ExperienceSubmitted()),
|
||||
: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceSubmitted()),
|
||||
fullWidth: true,
|
||||
text: state.status == ExperienceStatus.loading
|
||||
? null
|
||||
: i18n.save_button,
|
||||
child: state.status == ExperienceStatus.loading
|
||||
text: isLoading ? null : i18n.save_button as String,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: UiConstants.iconMd,
|
||||
width: UiConstants.iconMd,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
UiColors.white,
|
||||
), // UiColors.primaryForeground is white mostly
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(UiColors.white),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -245,4 +165,66 @@ class ExperiencePage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Maps a [StaffIndustry] to its localized label.
|
||||
String _getIndustryLabel(dynamic node, StaffIndustry industry) {
|
||||
switch (industry) {
|
||||
case StaffIndustry.hospitality:
|
||||
return node.hospitality as String;
|
||||
case StaffIndustry.foodService:
|
||||
return node.food_service as String;
|
||||
case StaffIndustry.warehouse:
|
||||
return node.warehouse as String;
|
||||
case StaffIndustry.events:
|
||||
return node.events as String;
|
||||
case StaffIndustry.retail:
|
||||
return node.retail as String;
|
||||
case StaffIndustry.healthcare:
|
||||
return node.healthcare as String;
|
||||
case StaffIndustry.catering:
|
||||
return node.catering as String;
|
||||
case StaffIndustry.cafe:
|
||||
return node.cafe as String;
|
||||
case StaffIndustry.other:
|
||||
return node.other as String;
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a [StaffSkill] to its localized label.
|
||||
String _getSkillLabel(dynamic node, StaffSkill skill) {
|
||||
switch (skill) {
|
||||
case StaffSkill.foodService:
|
||||
return node.food_service as String;
|
||||
case StaffSkill.bartending:
|
||||
return node.bartending as String;
|
||||
case StaffSkill.eventSetup:
|
||||
return node.event_setup as String;
|
||||
case StaffSkill.hospitality:
|
||||
return node.hospitality as String;
|
||||
case StaffSkill.warehouse:
|
||||
return node.warehouse as String;
|
||||
case StaffSkill.customerService:
|
||||
return node.customer_service as String;
|
||||
case StaffSkill.cleaning:
|
||||
return node.cleaning as String;
|
||||
case StaffSkill.security:
|
||||
return node.security as String;
|
||||
case StaffSkill.retail:
|
||||
return node.retail as String;
|
||||
case StaffSkill.driving:
|
||||
return node.driving as String;
|
||||
case StaffSkill.cooking:
|
||||
return node.cooking as String;
|
||||
case StaffSkill.cashier:
|
||||
return node.cashier as String;
|
||||
case StaffSkill.server:
|
||||
return node.server as String;
|
||||
case StaffSkill.barista:
|
||||
return node.barista as String;
|
||||
case StaffSkill.hostHostess:
|
||||
return node.host_hostess as String;
|
||||
case StaffSkill.busser:
|
||||
return node.busser as String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user