feat: enhance experience management by introducing ExperienceSkill and Industry enums, refactoring related components

This commit is contained in:
Achintha Isuru
2026-01-27 14:34:09 -05:00
parent 16bac72a4e
commit 93779c21bb
14 changed files with 161 additions and 165 deletions

View File

@@ -53,6 +53,7 @@ export 'src/entities/financial/staff_payment.dart';
export 'src/entities/profile/staff_document.dart';
export 'src/entities/profile/attire_item.dart';
export 'src/entities/profile/relationship_type.dart';
export 'src/entities/profile/industry.dart';
// Ratings & Penalties
export 'src/entities/ratings/staff_rating.dart';
@@ -82,3 +83,4 @@ export 'src/entities/availability/day_availability.dart';
// Adapters
export 'src/adapters/profile/emergency_contact_adapter.dart';
export 'src/adapters/profile/experience_adapter.dart';
export 'src/entities/profile/experience_skill.dart';

View File

@@ -0,0 +1,30 @@
enum ExperienceSkill {
foodService('food_service'),
bartending('bartending'),
eventSetup('event_setup'),
hospitality('hospitality'),
warehouse('warehouse'),
customerService('customer_service'),
cleaning('cleaning'),
security('security'),
retail('retail'),
cooking('cooking'),
cashier('cashier'),
server('server'),
barista('barista'),
hostHostess('host_hostess'),
busser('busser'),
driving('driving');
final String value;
const ExperienceSkill(this.value);
static ExperienceSkill? fromString(String value) {
try {
return ExperienceSkill.values.firstWhere((e) => e.value == value);
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,21 @@
enum Industry {
hospitality('hospitality'),
foodService('food_service'),
warehouse('warehouse'),
events('events'),
retail('retail'),
healthcare('healthcare'),
other('other');
final String value;
const Industry(this.value);
static Industry? fromString(String value) {
try {
return Industry.values.firstWhere((e) => e.value == value);
} catch (_) {
return null;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
import 'package:staff_authentication/staff_authentication.dart';
@@ -17,28 +18,6 @@ class ProfileSetupExperience extends StatelessWidget {
/// Callback for when industries change.
final ValueChanged<List<String>> onIndustriesChanged;
static const List<String> _allSkillKeys = <String>[
'food_service',
'bartending',
'warehouse',
'retail',
'events',
'customer_service',
'cleaning',
'security',
'driving',
'cooking',
];
static const List<String> _allIndustryKeys = <String>[
'hospitality',
'food_service',
'warehouse',
'events',
'retail',
'healthcare',
];
/// Creates a [ProfileSetupExperience] widget.
const ProfileSetupExperience({
super.key,
@@ -92,15 +71,15 @@ class ProfileSetupExperience extends StatelessWidget {
Wrap(
spacing: UiConstants.space2,
runSpacing: UiConstants.space2,
children: _allSkillKeys.map((String key) {
final bool isSelected = skills.contains(key);
children: ExperienceSkill.values.map((ExperienceSkill skill) {
final bool isSelected = skills.contains(skill.value);
// Dynamic translation access
final String label = _getSkillLabel(key);
final String label = _getSkillLabel(skill);
return UiChip(
label: label,
isSelected: isSelected,
onTap: () => _toggleSkill(skill: key),
onTap: () => _toggleSkill(skill: skill.value),
leadingIcon: isSelected ? UiIcons.check : null,
variant: UiChipVariant.primary,
);
@@ -118,14 +97,14 @@ class ProfileSetupExperience extends StatelessWidget {
Wrap(
spacing: UiConstants.space2,
runSpacing: UiConstants.space2,
children: _allIndustryKeys.map((String key) {
final bool isSelected = industries.contains(key);
final String label = _getIndustryLabel(key);
children: Industry.values.map((Industry industry) {
final bool isSelected = industries.contains(industry.value);
final String label = _getIndustryLabel(industry);
return UiChip(
label: label,
isSelected: isSelected,
onTap: () => _toggleIndustry(industry: key),
onTap: () => _toggleIndustry(industry: industry.value),
leadingIcon: isSelected ? UiIcons.check : null,
variant: isSelected
? UiChipVariant.accent
@@ -137,72 +116,74 @@ class ProfileSetupExperience extends StatelessWidget {
);
}
String _getSkillLabel(String key) {
switch (key) {
case 'food_service':
String _getSkillLabel(ExperienceSkill skill) {
switch (skill) {
case ExperienceSkill.foodService:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.food_service;
case 'bartending':
case ExperienceSkill.bartending:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.bartending;
case 'warehouse':
case ExperienceSkill.warehouse:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.warehouse;
case 'retail':
case ExperienceSkill.retail:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.retail;
case 'events':
// Note: 'events' was removed from enum in favor of 'event_setup' or industry.
// Using 'events' translation for eventSetup if available or fallback.
case ExperienceSkill.eventSetup:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.events;
case 'customer_service':
case ExperienceSkill.customerService:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.customer_service;
case 'cleaning':
case ExperienceSkill.cleaning:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.cleaning;
case 'security':
case ExperienceSkill.security:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.security;
case 'driving':
case ExperienceSkill.driving:
return t
.staff_authentication
.profile_setup_page
.experience
.skills
.driving;
case 'cooking':
case ExperienceSkill.cooking:
return t
.staff_authentication
.profile_setup_page
@@ -210,48 +191,48 @@ class ProfileSetupExperience extends StatelessWidget {
.skills
.cooking;
default:
return key;
return skill.value;
}
}
String _getIndustryLabel(String key) {
switch (key) {
case 'hospitality':
String _getIndustryLabel(Industry industry) {
switch (industry) {
case Industry.hospitality:
return t
.staff_authentication
.profile_setup_page
.experience
.industries
.hospitality;
case 'food_service':
case Industry.foodService:
return t
.staff_authentication
.profile_setup_page
.experience
.industries
.food_service;
case 'warehouse':
case Industry.warehouse:
return t
.staff_authentication
.profile_setup_page
.experience
.industries
.warehouse;
case 'events':
case Industry.events:
return t
.staff_authentication
.profile_setup_page
.experience
.industries
.events;
case 'retail':
case Industry.retail:
return t
.staff_authentication
.profile_setup_page
.experience
.industries
.retail;
case 'healthcare':
case Industry.healthcare:
return t
.staff_authentication
.profile_setup_page
@@ -259,7 +240,7 @@ class ProfileSetupExperience extends StatelessWidget {
.industries
.healthcare;
default:
return key;
return industry.value;
}
}
}

View File

@@ -15,8 +15,12 @@ class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<dc.GetStaffByIdStaff> _getStaff(String staffId) async {
final result = await _dataConnect.getStaffById(id: staffId).execute();
Future<dc.GetStaffByIdStaff> _getStaff() async {
final user = _firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
}
final result = await _dataConnect.getStaffById(id: user.uid).execute();
if (result.data.staff == null) {
throw Exception('Staff profile not found');
}
@@ -24,27 +28,31 @@ class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
}
@override
Future<List<String>> getIndustries(String staffId) async {
final staff = await _getStaff(staffId);
Future<List<String>> getIndustries() async {
final staff = await _getStaff();
return staff.industries ?? [];
}
@override
Future<List<String>> getSkills(String staffId) async {
final staff = await _getStaff(staffId);
Future<List<String>> getSkills() async {
final staff = await _getStaff();
return staff.skills ?? [];
}
@override
Future<void> saveExperience(
String staffId,
List<String> industries,
List<String> skills,
) async {
try {
final staff = await _getStaff();
await _dataConnect
.updateStaff(id: staffId)
.updateStaff(id: staff.id)
.industries(industries)
.skills(skills)
.execute();
} catch (e) {
throw Exception('Failed to save experience: $e');
}
}
}

View File

@@ -1,10 +0,0 @@
import 'package:krow_core/core.dart';
class GetExperienceArguments extends UseCaseArgument {
final String staffId;
GetExperienceArguments({required this.staffId});
@override
List<Object?> get props => [staffId];
}

View File

@@ -1,16 +1,14 @@
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];
List<Object?> get props => [industries, skills];
}

View File

@@ -1,14 +1,13 @@
/// 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);
Future<List<String>> getIndustries();
/// Fetches the list of skills associated with the staff member.
Future<List<String>> getSkills(String staffId);
Future<List<String>> getSkills();
/// Saves the staff member's experience (industries and skills).
Future<void> saveExperience(
String staffId,
List<String> industries,
List<String> skills,
);

View File

@@ -1,15 +1,14 @@
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>> {
class GetStaffIndustriesUseCase implements NoInputUseCase<List<String>> {
final ExperienceRepositoryInterface _repository;
GetStaffIndustriesUseCase(this._repository);
@override
Future<List<String>> call(GetExperienceArguments input) {
return _repository.getIndustries(input.staffId);
Future<List<String>> call() {
return _repository.getIndustries();
}
}

View File

@@ -1,15 +1,14 @@
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>> {
class GetStaffSkillsUseCase implements NoInputUseCase<List<String>> {
final ExperienceRepositoryInterface _repository;
GetStaffSkillsUseCase(this._repository);
@override
Future<List<String>> call(GetExperienceArguments input) {
return _repository.getSkills(input.staffId);
Future<List<String>> call() {
return _repository.getSkills();
}
}

View File

@@ -14,7 +14,6 @@ class SaveExperienceUseCase extends UseCase<SaveExperienceArguments, void> {
@override
Future<void> call(SaveExperienceArguments params) {
return repository.saveExperience(
params.staffId,
params.industries,
params.skills,
);

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/get_experience_arguments.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/save_experience_arguments.dart';
import '../../domain/usecases/get_staff_industries_usecase.dart';
import '../../domain/usecases/get_staff_skills_usecase.dart';
@@ -17,7 +17,7 @@ abstract class ExperienceEvent extends Equatable {
class ExperienceLoaded extends ExperienceEvent {}
class ExperienceIndustryToggled extends ExperienceEvent {
final String industry;
final Industry industry;
const ExperienceIndustryToggled(this.industry);
@override
@@ -47,10 +47,10 @@ enum ExperienceStatus { initial, loading, success, failure }
class ExperienceState extends Equatable {
final ExperienceStatus status;
final List<String> selectedIndustries;
final List<Industry> selectedIndustries;
final List<String> selectedSkills;
final List<String> availableIndustries;
final List<String> availableSkills;
final List<Industry> availableIndustries;
final List<ExperienceSkill> availableSkills;
final String? errorMessage;
const ExperienceState({
@@ -64,10 +64,10 @@ class ExperienceState extends Equatable {
ExperienceState copyWith({
ExperienceStatus? status,
List<String>? selectedIndustries,
List<Industry>? selectedIndustries,
List<String>? selectedSkills,
List<String>? availableIndustries,
List<String>? availableSkills,
List<Industry>? availableIndustries,
List<ExperienceSkill>? availableSkills,
String? errorMessage,
}) {
return ExperienceState(
@@ -93,47 +93,18 @@ class ExperienceState extends Equatable {
// 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,
availableIndustries: Industry.values,
availableSkills: ExperienceSkill.values,
)) {
on<ExperienceLoaded>(_onLoaded);
on<ExperienceIndustryToggled>(_onIndustryToggled);
@@ -148,15 +119,17 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
) async {
emit(state.copyWith(status: ExperienceStatus.loading));
try {
final arguments = GetExperienceArguments(staffId: staffId);
final results = await Future.wait([
getIndustries(arguments),
getSkills(arguments),
getIndustries(),
getSkills(),
]);
emit(state.copyWith(
status: ExperienceStatus.initial,
selectedIndustries: results[0],
selectedIndustries: results[0]
.map((e) => Industry.fromString(e))
.whereType<Industry>()
.toList(),
selectedSkills: results[1],
));
} catch (e) {
@@ -171,7 +144,7 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
ExperienceIndustryToggled event,
Emitter<ExperienceState> emit,
) {
final industries = List<String>.from(state.selectedIndustries);
final industries = List<Industry>.from(state.selectedIndustries);
if (industries.contains(event.industry)) {
industries.remove(event.industry);
} else {
@@ -211,8 +184,7 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
try {
await saveExperience(
SaveExperienceArguments(
staffId: staffId,
industries: state.selectedIndustries,
industries: state.selectedIndustries.map((e) => e.value).toList(),
skills: state.selectedSkills,
),
);

View File

@@ -3,6 +3,7 @@ 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 'package:krow_domain/krow_domain.dart';
import '../blocs/experience_bloc.dart';
import '../widgets/experience_custom_input.dart';
import '../widgets/experience_section_title.dart';
@@ -22,37 +23,36 @@ class ExperiencePage extends StatelessWidget {
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 _getIndustryLabel(dynamic node, Industry industry) {
switch (industry) {
case Industry.hospitality: return node.hospitality;
case Industry.foodService: return node.food_service;
case Industry.warehouse: return node.warehouse;
case Industry.events: return node.events;
case Industry.retail: return node.retail;
case Industry.healthcare: return node.healthcare;
case Industry.other: return node.other;
}
}
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;
String _getSkillLabel(dynamic node, ExperienceSkill skill) {
switch (skill) {
case ExperienceSkill.foodService: return node.food_service;
case ExperienceSkill.bartending: return node.bartending;
case ExperienceSkill.eventSetup: return node.event_setup;
case ExperienceSkill.hospitality: return node.hospitality;
case ExperienceSkill.warehouse: return node.warehouse;
case ExperienceSkill.customerService: return node.customer_service;
case ExperienceSkill.cleaning: return node.cleaning;
case ExperienceSkill.security: return node.security;
case ExperienceSkill.retail: return node.retail;
case ExperienceSkill.driving: return node.driving;
case ExperienceSkill.cooking: return node.cooking;
case ExperienceSkill.cashier: return node.cashier;
case ExperienceSkill.server: return node.server;
case ExperienceSkill.barista: return node.barista;
case ExperienceSkill.hostHostess: return node.host_hostess;
case ExperienceSkill.busser: return node.busser;
}
}
@@ -118,10 +118,10 @@ class _ExperienceView extends StatelessWidget {
.map(
(s) => UiChip(
label: _getSkillLabel(i18n.skills, s),
isSelected: state.selectedSkills.contains(s),
isSelected: state.selectedSkills.contains(s.value),
onTap: () => BlocProvider.of<ExperienceBloc>(context)
.add(ExperienceSkillToggled(s)),
variant: state.selectedSkills.contains(s)
.add(ExperienceSkillToggled(s.value)),
variant: state.selectedSkills.contains(s.value)
? UiChipVariant.primary
: UiChipVariant.secondary,
),
@@ -147,7 +147,7 @@ class _ExperienceView extends StatelessWidget {
Widget _buildCustomSkillsList(ExperienceState state, dynamic i18n) {
final customSkills = state.selectedSkills
.where((s) => !state.availableSkills.contains(s))
.where((s) => !state.availableSkills.any((e) => e.value == s))
.toList();
if (customSkills.isEmpty) return const SizedBox.shrink();

View File

@@ -45,8 +45,6 @@ class StaffProfileExperienceModule extends Module {
getIndustries: i.get<GetStaffIndustriesUseCase>(),
getSkills: i.get<GetStaffSkillsUseCase>(),
saveExperience: i.get<SaveExperienceUseCase>(),
// TODO: Get actual logged in staff ID
staffId: 'current-staff-id',
),
);
}