Merge branch '216-p0-staff-01-profile-setup-wizard' into coverage_screen_app

This commit is contained in:
José Salazar
2026-01-27 16:15:01 -05:00
56 changed files with 19682 additions and 19609 deletions

View File

@@ -1,9 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import 'profile_setup_event.dart';
import 'profile_setup_state.dart';
@@ -104,13 +104,13 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
.bio(state.bio.isEmpty ? null : state.bio)
.preferredLocations(state.preferredLocations)
.maxDistanceMiles(state.maxDistanceMiles.toInt())
.industries(fdc.AnyValue(state.industries))
.skills(fdc.AnyValue(state.skills))
.industries(state.industries)
.skills(state.skills)
.email(email.isEmpty ? null : email)
.phone(phone)
.execute();
final String staffId = result.data?.staff_insert.id ?? '';
final String staffId = result.data.staff_insert.id ;
final Staff staff = Staff(
id: staffId,
authProviderId: firebaseUser.uid,

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

@@ -2,58 +2,47 @@ import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/bank_account_repository.dart';
/// Implementation of [BankAccountRepository].
/// Implementation of [BankAccountRepository] that integrates with Data Connect.
class BankAccountRepositoryImpl implements BankAccountRepository {
/// Creates a [BankAccountRepositoryImpl].
const BankAccountRepositoryImpl({
required this.dataConnect,
required this.firebaseAuth,
});
/// The Data Connect instance.
final ExampleConnector dataConnect;
/// The Firebase Auth instance.
final auth.FirebaseAuth firebaseAuth;
@override
Future<List<BankAccount>> getAccounts() async {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final String? staffId = StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
print('BankAccount getAccounts: missing staffId userId=${user.uid} session=${StaffSessionStore.instance.session}');
throw Exception('Staff profile is missing.');
}
final String staffId = _getStaffId();
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
result = await dataConnect
.getAccountsByOwnerId(ownerId: staffId)
.execute();
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
return BankAccount(
return BankAccountAdapter.fromPrimitives(
id: account.id,
userId: account.ownerId,
bankName: account.bank,
accountNumber: account.accountNumber ?? '',
accountNumber: account.accountNumber,
last4: account.last4,
accountName: '', // Not returned by API
sortCode: account.routeNumber,
type: _mapAccountType(account.type),
isPrimary: account.isPrimary ?? false,
type: account.type is Known<AccountType> ? (account.type as Known<AccountType>).value.name : null,
isPrimary: account.isPrimary,
);
}).toList();
}
@override
Future<void> addAccount(BankAccount account) async {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final String? staffId = StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
print('BankAccount addAccount: missing staffId userId=${user.uid} session=${StaffSessionStore.instance.session}');
throw Exception('Staff profile is missing.');
}
final String staffId = _getStaffId();
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
existingAccounts = await dataConnect
@@ -64,44 +53,41 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
await dataConnect.createAccount(
bank: account.bankName,
type: _mapDomainType(account.type),
type: AccountType.values.byName(BankAccountAdapter.typeToString(account.type)),
last4: _safeLast4(account.last4, account.accountNumber),
ownerId: staffId,
).isPrimary(isPrimary).accountNumber(account.accountNumber).routeNumber(account.sortCode).execute();
)
.isPrimary(isPrimary)
.accountNumber(account.accountNumber)
.routeNumber(account.sortCode)
.execute();
}
BankAccountType _mapAccountType(EnumValue<AccountType> type) {
if (type is Known<AccountType>) {
switch (type.value) {
case AccountType.CHECKING:
return BankAccountType.checking;
case AccountType.SAVINGS:
return BankAccountType.savings;
}
/// Helper to get the logged-in staff ID.
String _getStaffId() {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
}
return BankAccountType.other;
}
AccountType _mapDomainType(BankAccountType type) {
switch (type) {
case BankAccountType.checking:
return AccountType.CHECKING;
case BankAccountType.savings:
return AccountType.SAVINGS;
default:
return AccountType.CHECKING; // Default fallback
final String? staffId = StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
throw Exception('Staff profile is missing or session not initialized.');
}
return staffId;
}
/// Ensures we have a last4 value, either from input or derived from account number.
String _safeLast4(String? last4, String accountNumber) {
if (last4 != null && last4.isNotEmpty) {
return last4;
}
if (accountNumber.isEmpty) {
return '';
return '0000';
}
return accountNumber.length > 4
? accountNumber.substring(accountNumber.length - 4)
: accountNumber;
if (accountNumber.length < 4) {
return accountNumber.padLeft(4, '0');
}
return accountNumber.substring(accountNumber.length - 4);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/add_bank_account_params.dart';
import '../../domain/usecases/add_bank_account_usecase.dart';
@@ -20,7 +19,7 @@ class BankAccountCubit extends Cubit<BankAccountState> {
Future<void> loadAccounts() async {
emit(state.copyWith(status: BankAccountStatus.loading));
try {
final accounts = await _getBankAccountsUseCase();
final List<BankAccount> accounts = await _getBankAccountsUseCase();
emit(state.copyWith(
status: BankAccountStatus.loaded,
accounts: accounts,
@@ -45,7 +44,7 @@ class BankAccountCubit extends Cubit<BankAccountState> {
emit(state.copyWith(status: BankAccountStatus.loading));
// Create domain entity
final newAccount = BankAccount(
final BankAccount newAccount = BankAccount(
id: '', // Generated by server usually
userId: '', // Handled by Repo/Auth
bankName: 'New Bank', // Mock

View File

@@ -1,25 +1,83 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/emergency_contact_repository_interface.dart';
/// Implementation of [EmergencyContactRepositoryInterface].
///
/// This repository delegates data operations to the [ProfileRepositoryMock]
/// (or real implementation) from the `data_connect` package.
/// This repository delegates data operations to Firebase Data Connect.
class EmergencyContactRepositoryImpl
implements EmergencyContactRepositoryInterface {
final ProfileRepositoryMock _profileRepository;
final dc.ExampleConnector _dataConnect;
final FirebaseAuth _firebaseAuth;
/// Creates an [EmergencyContactRepositoryImpl].
EmergencyContactRepositoryImpl(this._profileRepository);
EmergencyContactRepositoryImpl({
required dc.ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
@override
Future<List<EmergencyContact>> getContacts(String staffId) {
return _profileRepository.getEmergencyContacts(staffId);
Future<String> _getStaffId() async {
final user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
}
return result.data.staffs.first.id;
}
@override
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts) {
return _profileRepository.saveEmergencyContacts(staffId, contacts);
Future<List<EmergencyContact>> getContacts() async {
final staffId = await _getStaffId();
final result =
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
return result.data.emergencyContacts.map((dto) {
return EmergencyContactAdapter.fromPrimitives(
id: dto.id,
name: dto.name,
phone: dto.phone,
relationship: dto.relationship.stringValue,
);
}).toList();
}
}
@override
Future<void> saveContacts(List<EmergencyContact> contacts) async {
final staffId = await _getStaffId();
// 1. Get existing to delete
final existingResult =
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
final existingIds =
existingResult.data.emergencyContacts.map((e) => e.id).toList();
// 2. Delete all existing
await Future.wait(existingIds.map(
(id) => _dataConnect.deleteEmergencyContact(id: id).execute()));
// 3. Create new
await Future.wait(contacts.map((contact) {
dc.RelationshipType rel = dc.RelationshipType.OTHER;
switch(contact.relationship) {
case RelationshipType.family: rel = dc.RelationshipType.FAMILY; break;
case RelationshipType.spouse: rel = dc.RelationshipType.SPOUSE; break;
case RelationshipType.friend: rel = dc.RelationshipType.FRIEND; break;
case RelationshipType.other: rel = dc.RelationshipType.OTHER; break;
}
return _dataConnect
.createEmergencyContact(
name: contact.name,
phone: contact.phone,
relationship: rel,
staffId: staffId,
)
.execute();
}));
}
}

View File

@@ -2,12 +2,9 @@ import 'package:krow_core/core.dart';
/// Arguments for getting emergency contacts use case.
class GetEmergencyContactsArguments extends UseCaseArgument {
/// The ID of the staff member.
final String staffId;
/// Creates a [GetEmergencyContactsArguments].
const GetEmergencyContactsArguments({required this.staffId});
const GetEmergencyContactsArguments();
@override
List<Object?> get props => [staffId];
List<Object?> get props => [];
}

View File

@@ -3,18 +3,14 @@ import 'package:krow_domain/krow_domain.dart';
/// Arguments for saving emergency contacts use case.
class SaveEmergencyContactsArguments extends UseCaseArgument {
/// The ID of the staff member.
final String staffId;
/// The list of contacts to save.
final List<EmergencyContact> contacts;
/// Creates a [SaveEmergencyContactsArguments].
const SaveEmergencyContactsArguments({
required this.staffId,
required this.contacts,
});
@override
List<Object?> get props => [staffId, contacts];
List<Object?> get props => [contacts];
}

View File

@@ -1,26 +0,0 @@
import 'package:krow_domain/krow_domain.dart';
/// Extensions for [EmergencyContact] to support UI operations.
extension EmergencyContactExtensions on EmergencyContact {
/// returns a copy of this [EmergencyContact] with the given fields replaced.
EmergencyContact copyWith({
String? name,
String? phone,
String? relationship,
}) {
return EmergencyContact(
name: name ?? this.name,
phone: phone ?? this.phone,
relationship: relationship ?? this.relationship,
);
}
/// Returns an empty [EmergencyContact].
static EmergencyContact empty() {
return const EmergencyContact(
name: '',
phone: '',
relationship: 'family',
);
}
}

View File

@@ -6,8 +6,8 @@ import 'package:krow_domain/krow_domain.dart';
/// It must be implemented by the data layer.
abstract class EmergencyContactRepositoryInterface {
/// Retrieves the list of emergency contacts.
Future<List<EmergencyContact>> getContacts(String staffId);
Future<List<EmergencyContact>> getContacts();
/// Saves the list of emergency contacts.
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts);
Future<void> saveContacts(List<EmergencyContact> contacts);
}

View File

@@ -16,6 +16,6 @@ class GetEmergencyContactsUseCase
@override
Future<List<EmergencyContact>> call(GetEmergencyContactsArguments params) {
return _repository.getContacts(params.staffId);
return _repository.getContacts();
}
}

View File

@@ -15,6 +15,6 @@ class SaveEmergencyContactsUseCase
@override
Future<void> call(SaveEmergencyContactsArguments params) {
return _repository.saveContacts(params.staffId, params.contacts);
return _repository.saveContacts(params.contacts);
}
}

View File

@@ -1,99 +1,34 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/get_emergency_contacts_arguments.dart';
import '../../domain/arguments/save_emergency_contacts_arguments.dart';
import '../../domain/usecases/get_emergency_contacts_usecase.dart';
import '../../domain/usecases/save_emergency_contacts_usecase.dart';
import 'emergency_contact_event.dart';
import 'emergency_contact_state.dart';
// Events
abstract class EmergencyContactEvent extends Equatable {
const EmergencyContactEvent();
@override
List<Object?> get props => [];
}
class EmergencyContactsLoaded extends EmergencyContactEvent {}
class EmergencyContactAdded extends EmergencyContactEvent {}
class EmergencyContactRemoved extends EmergencyContactEvent {
final int index;
const EmergencyContactRemoved(this.index);
@override
List<Object?> get props => [index];
}
class EmergencyContactUpdated extends EmergencyContactEvent {
final int index;
final EmergencyContact contact;
const EmergencyContactUpdated(this.index, this.contact);
@override
List<Object?> get props => [index, contact];
}
class EmergencyContactsSaved extends EmergencyContactEvent {}
// State
enum EmergencyContactStatus { initial, loading, success, saving, failure }
class EmergencyContactState extends Equatable {
final EmergencyContactStatus status;
final List<EmergencyContact> contacts;
final String? errorMessage;
const EmergencyContactState({
this.status = EmergencyContactStatus.initial,
this.contacts = const [],
this.errorMessage,
});
EmergencyContactState copyWith({
EmergencyContactStatus? status,
List<EmergencyContact>? contacts,
String? errorMessage,
}) {
return EmergencyContactState(
status: status ?? this.status,
contacts: contacts ?? this.contacts,
errorMessage: errorMessage ?? this.errorMessage,
);
}
bool get isValid {
if (contacts.isEmpty) return false;
// Check if at least one contact is valid (or all?)
// Usually all added contacts should be valid.
return contacts.every((c) => c.name.isNotEmpty && c.phone.isNotEmpty);
}
@override
List<Object?> get props => [status, contacts, errorMessage];
}
export 'emergency_contact_event.dart';
export 'emergency_contact_state.dart';
// BLoC
class EmergencyContactBloc
extends Bloc<EmergencyContactEvent, EmergencyContactState> {
final GetEmergencyContactsUseCase getEmergencyContacts;
final SaveEmergencyContactsUseCase saveEmergencyContacts;
final String staffId;
EmergencyContactBloc({
required this.getEmergencyContacts,
required this.saveEmergencyContacts,
required this.staffId,
}) : super(const EmergencyContactState()) {
on<EmergencyContactsLoaded>(_onLoaded);
on<EmergencyContactAdded>(_onAdded);
on<EmergencyContactRemoved>(_onRemoved);
on<EmergencyContactUpdated>(_onUpdated);
on<EmergencyContactsSaved>(_onSaved);
add(EmergencyContactsLoaded());
}
Future<void> _onLoaded(
EmergencyContactsLoaded event,
@@ -102,13 +37,13 @@ class EmergencyContactBloc
emit(state.copyWith(status: EmergencyContactStatus.loading));
try {
final contacts = await getEmergencyContacts(
GetEmergencyContactsArguments(staffId: staffId),
const GetEmergencyContactsArguments(),
);
emit(state.copyWith(
status: EmergencyContactStatus.success,
status: EmergencyContactStatus.loaded,
contacts: contacts.isNotEmpty
? contacts
: [const EmergencyContact(name: '', phone: '', relationship: 'family')],
: [EmergencyContact.empty()],
));
} catch (e) {
emit(state.copyWith(
@@ -123,7 +58,7 @@ class EmergencyContactBloc
Emitter<EmergencyContactState> emit,
) {
final updatedContacts = List<EmergencyContact>.from(state.contacts)
..add(const EmergencyContact(name: '', phone: '', relationship: 'family'));
..add(EmergencyContact.empty());
emit(state.copyWith(contacts: updatedContacts));
}
@@ -153,11 +88,10 @@ class EmergencyContactBloc
try {
await saveEmergencyContacts(
SaveEmergencyContactsArguments(
staffId: staffId,
contacts: state.contacts,
),
);
emit(state.copyWith(status: EmergencyContactStatus.success));
emit(state.copyWith(status: EmergencyContactStatus.saved));
} catch (e) {
emit(state.copyWith(
status: EmergencyContactStatus.failure,

View File

@@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class EmergencyContactEvent extends Equatable {
const EmergencyContactEvent();
@override
List<Object?> get props => [];
}
class EmergencyContactsLoaded extends EmergencyContactEvent {}
class EmergencyContactAdded extends EmergencyContactEvent {}
class EmergencyContactRemoved extends EmergencyContactEvent {
final int index;
const EmergencyContactRemoved(this.index);
@override
List<Object?> get props => [index];
}
class EmergencyContactUpdated extends EmergencyContactEvent {
final int index;
final EmergencyContact contact;
const EmergencyContactUpdated(this.index, this.contact);
@override
List<Object?> get props => [index, contact];
}
class EmergencyContactsSaved extends EmergencyContactEvent {}

View File

@@ -0,0 +1,38 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum EmergencyContactStatus { initial, loading, loaded, saving, saved, failure }
class EmergencyContactState extends Equatable {
final EmergencyContactStatus status;
final List<EmergencyContact> contacts;
final String? errorMessage;
const EmergencyContactState({
this.status = EmergencyContactStatus.initial,
this.contacts = const [],
this.errorMessage,
});
EmergencyContactState copyWith({
EmergencyContactStatus? status,
List<EmergencyContact>? contacts,
String? errorMessage,
}) {
return EmergencyContactState(
status: status ?? this.status,
contacts: contacts ?? this.contacts,
errorMessage: errorMessage ?? this.errorMessage,
);
}
bool get isValid {
if (contacts.isEmpty) return false;
// Check if at least one contact is valid (or all?)
// Usually all added contacts should be valid.
return contacts.every((c) => c.name.isNotEmpty && c.phone.isNotEmpty);
}
@override
List<Object?> get props => [status, contacts, errorMessage];
}

View File

@@ -17,19 +17,6 @@ import '../widgets/emergency_contact_save_button.dart';
class EmergencyContactScreen extends StatelessWidget {
const EmergencyContactScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) =>
Modular.get<EmergencyContactBloc>()..add(EmergencyContactsLoaded()),
child: const _EmergencyContactView(),
);
}
}
class _EmergencyContactView extends StatelessWidget {
const _EmergencyContactView();
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -48,44 +35,47 @@ class _EmergencyContactView extends StatelessWidget {
child: Container(color: UiColors.border, height: 1.0),
),
),
body: BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
listener: (context, state) {
if (state.status == EmergencyContactStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
);
}
},
builder: (context, state) {
if (state.status == EmergencyContactStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(UiConstants.space6),
child: Column(
children: [
const EmergencyContactInfoBanner(),
SizedBox(height: UiConstants.space6),
...state.contacts.asMap().entries.map(
(entry) => EmergencyContactFormItem(
index: entry.key,
contact: entry.value,
totalContacts: state.contacts.length,
body: BlocProvider(
create: (context) => Modular.get<EmergencyContactBloc>(),
child: BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
listener: (context, state) {
if (state.status == EmergencyContactStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
);
}
},
builder: (context, state) {
if (state.status == EmergencyContactStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(UiConstants.space6),
child: Column(
children: [
const EmergencyContactInfoBanner(),
SizedBox(height: UiConstants.space6),
...state.contacts.asMap().entries.map(
(entry) => EmergencyContactFormItem(
index: entry.key,
contact: entry.value,
totalContacts: state.contacts.length,
),
),
),
const EmergencyContactAddButton(),
SizedBox(height: UiConstants.space16),
],
const EmergencyContactAddButton(),
SizedBox(height: UiConstants.space16),
],
),
),
),
),
EmergencyContactSaveButton(state: state),
],
);
},
const EmergencyContactSaveButton(),
],
);
},
),
),
);
}

View File

@@ -2,7 +2,6 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/extensions/emergency_contact_extensions.dart';
import '../blocs/emergency_contact_bloc.dart';
class EmergencyContactFormItem extends StatelessWidget {
@@ -62,7 +61,7 @@ class EmergencyContactFormItem extends StatelessWidget {
_buildDropdown(
context,
value: contact.relationship,
items: const ['family', 'friend', 'partner', 'other'],
items: RelationshipType.values,
onChanged: (val) {
if (val != null) {
context.read<EmergencyContactBloc>().add(
@@ -79,6 +78,52 @@ class EmergencyContactFormItem extends StatelessWidget {
);
}
Widget _buildDropdown(
BuildContext context, {
required RelationshipType value,
required List<RelationshipType> items,
required ValueChanged<RelationshipType?> onChanged,
}) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space2,
),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<RelationshipType>(
value: value,
isExpanded: true,
dropdownColor: UiColors.bgPopup,
icon: Icon(UiIcons.chevronDown, color: UiColors.iconSecondary),
items: items.map((type) {
return DropdownMenuItem<RelationshipType>(
value: type,
child: Text(
_formatRelationship(type),
style: UiTypography.body1r.copyWith(color: UiColors.textPrimary),
),
);
}).toList(),
onChanged: onChanged,
),
),
);
}
String _formatRelationship(RelationshipType type) {
switch(type) {
case RelationshipType.family: return 'Family';
case RelationshipType.spouse: return 'Spouse';
case RelationshipType.friend: return 'Friend';
case RelationshipType.other: return 'Other';
}
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -150,39 +195,5 @@ class EmergencyContactFormItem extends StatelessWidget {
onChanged: onChanged,
);
}
Widget _buildDropdown(
BuildContext context, {
required String value,
required List<String> items,
required Function(String?) onChanged,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: items.contains(value) ? value : items.first,
isExpanded: true,
icon: Icon(UiIcons.chevronDown, color: UiColors.textSecondary),
items: items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(
item.toUpperCase(),
style: UiTypography.body1r.copyWith(
color: UiColors.textPrimary,
),
),
);
}).toList(),
onChanged: onChanged,
),
),
);
}
}

View File

@@ -4,54 +4,59 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/emergency_contact_bloc.dart';
class EmergencyContactSaveButton extends StatelessWidget {
final EmergencyContactState state;
const EmergencyContactSaveButton({super.key});
const EmergencyContactSaveButton({super.key, required this.state});
void _onSave(BuildContext context) {
context.read<EmergencyContactBloc>().add(EmergencyContactsSaved());
}
@override
Widget build(BuildContext context) {
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.isValid
? () => context
.read<EmergencyContactBloc>()
.add(EmergencyContactsSaved())
: null,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.primaryForeground,
disabledBackgroundColor: UiColors.textPlaceholder,
padding: EdgeInsets.symmetric(vertical: UiConstants.space4),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusFull,
return BlocConsumer<EmergencyContactBloc, EmergencyContactState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == EmergencyContactStatus.saved) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Emergency contacts saved successfully',
style: UiTypography.body2r.textPrimary,
),
elevation: 0,
backgroundColor: UiColors.iconSuccess,
),
child: state.status == EmergencyContactStatus.saving
? SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(UiColors.primaryForeground),
),
)
: Text(
'Save & Continue',
style: UiTypography.title2b,
),
);
}
},
builder: (context, state) {
final isLoading = state.status == EmergencyContactStatus.saving;
return Container(
padding: EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.bgPopup,
border: Border(top: BorderSide(color: UiColors.border)),
),
),
),
child: SafeArea(
child: UiButton.primary(
fullWidth: true,
onPressed: state.isValid && !isLoading
? () => _onSave(context)
: null,
child: isLoading
? SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
UiColors.primaryForeground,
),
),
)
: const Text('Save & Continue'),
),
),
);
},
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/emergency_contact_repository_impl.dart';
@@ -11,10 +12,11 @@ class StaffEmergencyContactModule extends Module {
@override
void binds(Injector i) {
// Repository
// Uses ProfileRepositoryMock from data_connect
i.addLazySingleton<ProfileRepositoryMock>(ProfileRepositoryMock.new);
i.addLazySingleton<EmergencyContactRepositoryInterface>(
() => EmergencyContactRepositoryImpl(i.get<ProfileRepositoryMock>()),
() => EmergencyContactRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases
@@ -26,11 +28,10 @@ class StaffEmergencyContactModule extends Module {
);
// BLoC
i.addLazySingleton<EmergencyContactBloc>(
i.add<EmergencyContactBloc>(
() => EmergencyContactBloc(
getEmergencyContacts: i.get<GetEmergencyContactsUseCase>(),
saveEmergencyContacts: i.get<SaveEmergencyContactsUseCase>(),
staffId: 'mock-staff-id', // TODO: Get direct from auth state
),
);
}

View File

@@ -1 +1 @@
# include: package:flutter_lints/flutter.yaml
include: package:flutter_lints/flutter.yaml

View File

@@ -1,29 +1,58 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import '../../domain/repositories/experience_repository_interface.dart';
/// Implementation of [ExperienceRepositoryInterface] that delegates to Data Connect.
class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
final ProfileRepositoryMock _mockRepository;
final dc.ExampleConnector _dataConnect;
// ignore: unused_field
final FirebaseAuth _firebaseAuth;
/// Creates a [ExperienceRepositoryImpl] with the given [ProfileRepositoryMock].
ExperienceRepositoryImpl(this._mockRepository);
/// Creates a [ExperienceRepositoryImpl] using Da a Connect and Auth.
ExperienceRepositoryImpl({
required dc.ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
@override
Future<List<String>> getIndustries(String staffId) {
return _mockRepository.getStaffIndustries(staffId);
Future<dc.GetStaffByUserIdStaffs> _getStaff() async {
final user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
}
return result.data.staffs.first;
}
@override
Future<List<String>> getSkills(String staffId) {
return _mockRepository.getStaffSkills(staffId);
Future<List<String>> getIndustries() async {
final staff = await _getStaff();
return staff.industries ?? [];
}
@override
Future<List<String>> getSkills() async {
final staff = await _getStaff();
return staff.skills ?? [];
}
@override
Future<void> saveExperience(
String staffId,
List<String> industries,
List<String> skills,
) {
return _mockRepository.saveExperience(staffId, industries, skills);
) async {
try {
final staff = await _getStaff();
await _dataConnect
.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,53 +93,26 @@ 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);
on<ExperienceSkillToggled>(_onSkillToggled);
on<ExperienceCustomSkillAdded>(_onCustomSkillAdded);
on<ExperienceSubmitted>(_onSubmitted);
add(ExperienceLoaded());
}
Future<void> _onLoaded(
@@ -148,15 +121,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 +146,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 +186,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,56 +3,44 @@ 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';
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 _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;
}
}
@@ -61,93 +49,97 @@ class _ExperienceView extends StatelessWidget {
final i18n = t.staff.onboarding.experience;
return Scaffold(
backgroundColor: UiColors.background,
appBar: UiAppBar(
title: i18n.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: BlocConsumer<ExperienceBloc, ExperienceState>(
listener: (context, state) {
body: BlocProvider<ExperienceBloc>(
create: (context) => Modular.get<ExperienceBloc>(),
child: BlocConsumer<ExperienceBloc, ExperienceState>(
listener: (context, state) {
if (state.status == ExperienceStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Experience saved successfully')),
);
Modular.to.pop();
} else if (state.status == ExperienceStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
);
}
},
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) => 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(),
),
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) => 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(),
),
SizedBox(height: UiConstants.space4),
const ExperienceCustomInput(),
SizedBox(height: UiConstants.space4),
_buildCustomSkillsList(state, i18n),
SizedBox(height: UiConstants.space10),
],
),
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) => 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(),
),
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) => UiChip(
label: _getSkillLabel(i18n.skills, s),
isSelected: state.selectedSkills.contains(s.value),
onTap: () => BlocProvider.of<ExperienceBloc>(context)
.add(ExperienceSkillToggled(s.value)),
variant: state.selectedSkills.contains(s.value)
? UiChipVariant.primary
: UiChipVariant.secondary,
),
)
.toList(),
),
],
),
),
),
),
_buildSaveButton(context, state, i18n),
],
);
},
_buildSaveButton(context, state, i18n),
],
);
},
),
),
);
}
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();
@@ -181,25 +173,22 @@ class _ExperienceView extends StatelessWidget {
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SafeArea(
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
onPressed: state.status == ExperienceStatus.loading
? null
: () => BlocProvider.of<ExperienceBloc>(context).add(ExperienceSubmitted()),
fullWidth: true,
text: state.status == ExperienceStatus.loading ? null : i18n.save_button,
child: state.status == ExperienceStatus.loading
? SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(UiColors.white), // UiColors.primaryForeground is white mostly
),
)
: null,
),
child: UiButton.primary(
onPressed: state.status == ExperienceStatus.loading
? null
: () => BlocProvider.of<ExperienceBloc>(context).add(ExperienceSubmitted()),
fullWidth: true,
text: state.status == ExperienceStatus.loading ? null : i18n.save_button,
child: state.status == ExperienceStatus.loading
? SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(UiColors.white), // UiColors.primaryForeground is white mostly
),
)
: null,
),
),
);

View File

@@ -1,5 +1,6 @@
library staff_profile_experience;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
@@ -21,7 +22,10 @@ class StaffProfileExperienceModule extends Module {
void binds(Injector i) {
// Repository
i.addLazySingleton<ExperienceRepositoryInterface>(
() => ExperienceRepositoryImpl(i.get<ProfileRepositoryMock>()),
() => ExperienceRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases
@@ -36,13 +40,11 @@ class StaffProfileExperienceModule extends Module {
);
// BLoC
i.addLazySingleton<ExperienceBloc>(
i.add<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',
),
);
}

View File

@@ -17,15 +17,16 @@ dependencies:
# Architecture Packages
krow_domain:
path: ../../../../../../domain
path: ../../../../../domain
krow_core:
path: ../../../../../../core
path: ../../../../../core
krow_data_connect:
path: ../../../../../../data_connect
path: ../../../../../data_connect
firebase_auth: ^6.1.2
design_system:
path: ../../../../../../design_system
path: ../../../../../design_system
core_localization:
path: ../../../../../../core_localization
path: ../../../../../core_localization
dev_dependencies:
flutter_test: