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,7 +1,7 @@
name: krowwithus_client
description: "Krow Client Application"
publish_to: 'none'
version: 0.0.1+M301
publish_to: "none"
version: 0.0.1-M+301
resolution: workspace
environment:

View File

@@ -79,7 +79,12 @@
"cleaning": "Cleaning",
"security": "Security",
"driving": "Driving",
"cooking": "Cooking"
"cooking": "Cooking",
"cashier": "Cashier",
"server": "Server",
"barista": "Barista",
"host_hostess": "Host/Hostess",
"busser": "Busser"
},
"industries": {
"hospitality": "Hospitality",
@@ -577,7 +582,8 @@
"server": "Server",
"barista": "Barista",
"host_hostess": "Host/Hostess",
"busser": "Busser"
"busser": "Busser",
"driving": "Driving"
}
}
}

View File

@@ -79,7 +79,12 @@
"cleaning": "Limpieza",
"security": "Seguridad",
"driving": "Conducción",
"cooking": "Cocina"
"cooking": "Cocina",
"cashier": "Cajero",
"server": "Mesero",
"barista": "Barista",
"host_hostess": "Anfitrión",
"busser": "Ayudante de mesero"
},
"industries": {
"hospitality": "Hostelería",
@@ -576,7 +581,8 @@
"server": "Server",
"barista": "Barista",
"host_hostess": "Host/Hostess",
"busser": "Busser"
"busser": "Busser",
"driving": "Driving"
}
}
}

View File

@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1026 (513 per locale)
/// Strings: 1038 (519 per locale)
///
/// Built on 2026-01-27 at 00:15 UTC
/// Built on 2026-01-27 at 19:37 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View File

@@ -2420,6 +2420,21 @@ class TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEn {
/// en: 'Cooking'
String get cooking => 'Cooking';
/// en: 'Cashier'
String get cashier => 'Cashier';
/// en: 'Server'
String get server => 'Server';
/// en: 'Barista'
String get barista => 'Barista';
/// en: 'Host/Hostess'
String get host_hostess => 'Host/Hostess';
/// en: 'Busser'
String get busser => 'Busser';
}
// Path: staff_authentication.profile_setup_page.experience.industries
@@ -2589,6 +2604,9 @@ class TranslationsStaffOnboardingExperienceSkillsEn {
/// en: 'Busser'
String get busser => 'Busser';
/// en: 'Driving'
String get driving => 'Driving';
}
// Path: staff.home.improve.items.training
@@ -2724,6 +2742,11 @@ extension on Translations {
'staff_authentication.profile_setup_page.experience.skills.security' => 'Security',
'staff_authentication.profile_setup_page.experience.skills.driving' => 'Driving',
'staff_authentication.profile_setup_page.experience.skills.cooking' => 'Cooking',
'staff_authentication.profile_setup_page.experience.skills.cashier' => 'Cashier',
'staff_authentication.profile_setup_page.experience.skills.server' => 'Server',
'staff_authentication.profile_setup_page.experience.skills.barista' => 'Barista',
'staff_authentication.profile_setup_page.experience.skills.host_hostess' => 'Host/Hostess',
'staff_authentication.profile_setup_page.experience.skills.busser' => 'Busser',
'staff_authentication.profile_setup_page.experience.industries.hospitality' => 'Hospitality',
'staff_authentication.profile_setup_page.experience.industries.food_service' => 'Food Service',
'staff_authentication.profile_setup_page.experience.industries.warehouse' => 'Warehouse',
@@ -3090,6 +3113,7 @@ extension on Translations {
'staff.onboarding.experience.skills.barista' => 'Barista',
'staff.onboarding.experience.skills.host_hostess' => 'Host/Hostess',
'staff.onboarding.experience.skills.busser' => 'Busser',
'staff.onboarding.experience.skills.driving' => 'Driving',
'staff_documents.title' => 'Documents',
'staff_documents.verification_card.title' => 'Document Verification',
'staff_documents.verification_card.progress' => ({required Object completed, required Object total}) => '${completed}/${total} Complete',
@@ -3171,14 +3195,14 @@ extension on Translations {
'staff_time_card.hours_worked' => 'Hours Worked',
'staff_time_card.total_earnings' => 'Total Earnings',
'staff_time_card.shift_history' => 'Shift History',
_ => null,
} ?? switch (path) {
'staff_time_card.no_shifts' => 'No shifts for this month',
'staff_time_card.hours' => 'hours',
'staff_time_card.per_hr' => '/hr',
'staff_time_card.status.approved' => 'Approved',
'staff_time_card.status.disputed' => 'Disputed',
'staff_time_card.status.paid' => 'Paid',
_ => null,
} ?? switch (path) {
'staff_time_card.status.pending' => 'Pending',
_ => null,
};

View File

@@ -1463,6 +1463,11 @@ class _TranslationsStaffAuthenticationProfileSetupPageExperienceSkillsEs impleme
@override String get security => 'Seguridad';
@override String get driving => 'Conducción';
@override String get cooking => 'Cocina';
@override String get cashier => 'Cajero';
@override String get server => 'Mesero';
@override String get barista => 'Barista';
@override String get host_hostess => 'Anfitrión';
@override String get busser => 'Ayudante de mesero';
}
// Path: staff_authentication.profile_setup_page.experience.industries
@@ -1564,6 +1569,7 @@ class _TranslationsStaffOnboardingExperienceSkillsEs implements TranslationsStaf
@override String get barista => 'Barista';
@override String get host_hostess => 'Host/Hostess';
@override String get busser => 'Busser';
@override String get driving => 'Driving';
}
// Path: staff.home.improve.items.training
@@ -1679,6 +1685,11 @@ extension on TranslationsEs {
'staff_authentication.profile_setup_page.experience.skills.security' => 'Seguridad',
'staff_authentication.profile_setup_page.experience.skills.driving' => 'Conducción',
'staff_authentication.profile_setup_page.experience.skills.cooking' => 'Cocina',
'staff_authentication.profile_setup_page.experience.skills.cashier' => 'Cajero',
'staff_authentication.profile_setup_page.experience.skills.server' => 'Mesero',
'staff_authentication.profile_setup_page.experience.skills.barista' => 'Barista',
'staff_authentication.profile_setup_page.experience.skills.host_hostess' => 'Anfitrión',
'staff_authentication.profile_setup_page.experience.skills.busser' => 'Ayudante de mesero',
'staff_authentication.profile_setup_page.experience.industries.hospitality' => 'Hostelería',
'staff_authentication.profile_setup_page.experience.industries.food_service' => 'Servicio de comida',
'staff_authentication.profile_setup_page.experience.industries.warehouse' => 'Almacén',
@@ -2045,6 +2056,7 @@ extension on TranslationsEs {
'staff.onboarding.experience.skills.barista' => 'Barista',
'staff.onboarding.experience.skills.host_hostess' => 'Host/Hostess',
'staff.onboarding.experience.skills.busser' => 'Busser',
'staff.onboarding.experience.skills.driving' => 'Driving',
'staff_documents.title' => 'Documents',
'staff_documents.verification_card.title' => 'Document Verification',
'staff_documents.verification_card.progress' => ({required Object completed, required Object total}) => '${completed}/${total} Complete',
@@ -2126,14 +2138,14 @@ extension on TranslationsEs {
'staff_time_card.hours_worked' => 'Horas trabajadas',
'staff_time_card.total_earnings' => 'Ganancias totales',
'staff_time_card.shift_history' => 'Historial de turnos',
_ => null,
} ?? switch (path) {
'staff_time_card.no_shifts' => 'No hay turnos para este mes',
'staff_time_card.hours' => 'horas',
'staff_time_card.per_hr' => '/hr',
'staff_time_card.status.approved' => 'Aprobado',
'staff_time_card.status.disputed' => 'Disputado',
'staff_time_card.status.paid' => 'Pagado',
_ => null,
} ?? switch (path) {
'staff_time_card.status.pending' => 'Pendiente',
_ => null,
};

View File

@@ -1,16 +1,16 @@
# Basic Usage
```dart
ExampleConnector.instance.listActivityLogs(listActivityLogsVariables).execute();
ExampleConnector.instance.getActivityLogById(getActivityLogByIdVariables).execute();
ExampleConnector.instance.listActivityLogsByUserId(listActivityLogsByUserIdVariables).execute();
ExampleConnector.instance.listUnreadActivityLogsByUserId(listUnreadActivityLogsByUserIdVariables).execute();
ExampleConnector.instance.filterActivityLogs(filterActivityLogsVariables).execute();
ExampleConnector.instance.listConversations(listConversationsVariables).execute();
ExampleConnector.instance.getConversationById(getConversationByIdVariables).execute();
ExampleConnector.instance.listConversationsByType(listConversationsByTypeVariables).execute();
ExampleConnector.instance.listConversationsByStatus(listConversationsByStatusVariables).execute();
ExampleConnector.instance.filterConversations(filterConversationsVariables).execute();
ExampleConnector.instance.createTeamHudDepartment(createTeamHudDepartmentVariables).execute();
ExampleConnector.instance.updateTeamHudDepartment(updateTeamHudDepartmentVariables).execute();
ExampleConnector.instance.deleteTeamHudDepartment(deleteTeamHudDepartmentVariables).execute();
ExampleConnector.instance.CreateUser(createUserVariables).execute();
ExampleConnector.instance.UpdateUser(updateUserVariables).execute();
ExampleConnector.instance.DeleteUser(deleteUserVariables).execute();
ExampleConnector.instance.createVendorBenefitPlan(createVendorBenefitPlanVariables).execute();
ExampleConnector.instance.updateVendorBenefitPlan(updateVendorBenefitPlanVariables).execute();
ExampleConnector.instance.deleteVendorBenefitPlan(deleteVendorBenefitPlanVariables).execute();
ExampleConnector.instance.getShiftRoleById(getShiftRoleByIdVariables).execute();
```
@@ -23,8 +23,8 @@ Optional fields can be discovered based on classes that have `Optional` object t
This is an example of a mutation with an optional field:
```dart
await ExampleConnector.instance.createStaffRole({ ... })
.roleType(...)
await ExampleConnector.instance.updateStaffAvailabilityStats({ ... })
.needWorkIndex(...)
.execute();
```

View File

@@ -15,8 +15,8 @@ class CreateStaffVariablesBuilder {
Optional<int> _cancellationCount = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _reliabilityScore = Optional.optional(nativeFromJson, nativeToJson);
Optional<String> _bio = Optional.optional(nativeFromJson, nativeToJson);
Optional<AnyValue> _skills = Optional.optional(AnyValue.fromJson, defaultSerializer);
Optional<AnyValue> _industries = Optional.optional(AnyValue.fromJson, defaultSerializer);
Optional<List<String>> _skills = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<List<String>> _industries = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<List<String>> _preferredLocations = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<int> _maxDistanceMiles = Optional.optional(nativeFromJson, nativeToJson);
Optional<AnyValue> _languages = Optional.optional(AnyValue.fromJson, defaultSerializer);
@@ -84,11 +84,11 @@ class CreateStaffVariablesBuilder {
_bio.value = t;
return this;
}
CreateStaffVariablesBuilder skills(AnyValue? t) {
CreateStaffVariablesBuilder skills(List<String>? t) {
_skills.value = t;
return this;
}
CreateStaffVariablesBuilder industries(AnyValue? t) {
CreateStaffVariablesBuilder industries(List<String>? t) {
_industries.value = t;
return this;
}
@@ -262,8 +262,8 @@ class CreateStaffVariables {
late final Optional<int>cancellationCount;
late final Optional<int>reliabilityScore;
late final Optional<String>bio;
late final Optional<AnyValue>skills;
late final Optional<AnyValue>industries;
late final Optional<List<String>>skills;
late final Optional<List<String>>industries;
late final Optional<List<String>>preferredLocations;
late final Optional<int>maxDistanceMiles;
late final Optional<AnyValue>languages;
@@ -339,12 +339,16 @@ class CreateStaffVariables {
bio.value = json['bio'] == null ? null : nativeFromJson<String>(json['bio']);
skills = Optional.optional(AnyValue.fromJson, defaultSerializer);
skills.value = json['skills'] == null ? null : AnyValue.fromJson(json['skills']);
skills = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
skills.value = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList();
industries = Optional.optional(AnyValue.fromJson, defaultSerializer);
industries.value = json['industries'] == null ? null : AnyValue.fromJson(json['industries']);
industries = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
industries.value = json['industries'] == null ? null : (json['industries'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList();
preferredLocations = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));

View File

@@ -51,7 +51,7 @@ class FilterStaffStaffs {
final int? totalShifts;
final String? ownerId;
final bool? isRecommended;
final AnyValue? skills;
final List<String>? skills;
final EnumValue<BackgroundCheckStatus>? backgroundCheckStatus;
final EnumValue<EmploymentType>? employmentType;
final String? initial;
@@ -72,7 +72,9 @@ class FilterStaffStaffs {
totalShifts = json['totalShifts'] == null ? null : nativeFromJson<int>(json['totalShifts']),
ownerId = json['ownerId'] == null ? null : nativeFromJson<String>(json['ownerId']),
isRecommended = json['isRecommended'] == null ? null : nativeFromJson<bool>(json['isRecommended']),
skills = json['skills'] == null ? null : AnyValue.fromJson(json['skills']),
skills = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
backgroundCheckStatus = json['backgroundCheckStatus'] == null ? null : backgroundCheckStatusDeserializer(json['backgroundCheckStatus']),
employmentType = json['employmentType'] == null ? null : employmentTypeDeserializer(json['employmentType']),
initial = json['initial'] == null ? null : nativeFromJson<String>(json['initial']),
@@ -147,7 +149,7 @@ class FilterStaffStaffs {
json['isRecommended'] = nativeToJson<bool?>(isRecommended);
}
if (skills != null) {
json['skills'] = skills!.toJson();
json['skills'] = skills?.map((e) => nativeToJson<String>(e)).toList();
}
if (backgroundCheckStatus != null) {
json['backgroundCheckStatus'] =

View File

@@ -37,8 +37,8 @@ class GetStaffByIdStaff {
final AnyValue? badges;
final bool? isRecommended;
final String? bio;
final AnyValue? skills;
final AnyValue? industries;
final List<String>? skills;
final List<String>? industries;
final List<String>? preferredLocations;
final int? maxDistanceMiles;
final AnyValue? languages;
@@ -77,8 +77,12 @@ class GetStaffByIdStaff {
badges = json['badges'] == null ? null : AnyValue.fromJson(json['badges']),
isRecommended = json['isRecommended'] == null ? null : nativeFromJson<bool>(json['isRecommended']),
bio = json['bio'] == null ? null : nativeFromJson<String>(json['bio']),
skills = json['skills'] == null ? null : AnyValue.fromJson(json['skills']),
industries = json['industries'] == null ? null : AnyValue.fromJson(json['industries']),
skills = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
industries = json['industries'] == null ? null : (json['industries'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
preferredLocations = json['preferredLocations'] == null ? null : (json['preferredLocations'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
@@ -204,10 +208,10 @@ class GetStaffByIdStaff {
json['bio'] = nativeToJson<String?>(bio);
}
if (skills != null) {
json['skills'] = skills!.toJson();
json['skills'] = skills?.map((e) => nativeToJson<String>(e)).toList();
}
if (industries != null) {
json['industries'] = industries!.toJson();
json['industries'] = industries?.map((e) => nativeToJson<String>(e)).toList();
}
if (preferredLocations != null) {
json['preferredLocations'] = preferredLocations?.map((e) => nativeToJson<String>(e)).toList();

View File

@@ -36,8 +36,8 @@ class GetStaffByUserIdStaffs {
final AnyValue? badges;
final bool? isRecommended;
final String? bio;
final AnyValue? skills;
final AnyValue? industries;
final List<String>? skills;
final List<String>? industries;
final List<String>? preferredLocations;
final int? maxDistanceMiles;
final AnyValue? languages;
@@ -75,8 +75,12 @@ class GetStaffByUserIdStaffs {
badges = json['badges'] == null ? null : AnyValue.fromJson(json['badges']),
isRecommended = json['isRecommended'] == null ? null : nativeFromJson<bool>(json['isRecommended']),
bio = json['bio'] == null ? null : nativeFromJson<String>(json['bio']),
skills = json['skills'] == null ? null : AnyValue.fromJson(json['skills']),
industries = json['industries'] == null ? null : AnyValue.fromJson(json['industries']),
skills = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
industries = json['industries'] == null ? null : (json['industries'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
preferredLocations = json['preferredLocations'] == null ? null : (json['preferredLocations'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
@@ -198,10 +202,10 @@ class GetStaffByUserIdStaffs {
json['bio'] = nativeToJson<String?>(bio);
}
if (skills != null) {
json['skills'] = skills!.toJson();
json['skills'] = skills?.map((e) => nativeToJson<String>(e)).toList();
}
if (industries != null) {
json['industries'] = industries!.toJson();
json['industries'] = industries?.map((e) => nativeToJson<String>(e)).toList();
}
if (preferredLocations != null) {
json['preferredLocations'] = preferredLocations?.map((e) => nativeToJson<String>(e)).toList();

View File

@@ -36,8 +36,8 @@ class ListStaffStaffs {
final AnyValue? badges;
final bool? isRecommended;
final String? bio;
final AnyValue? skills;
final AnyValue? industries;
final List<String>? skills;
final List<String>? industries;
final List<String>? preferredLocations;
final int? maxDistanceMiles;
final AnyValue? languages;
@@ -74,8 +74,12 @@ class ListStaffStaffs {
badges = json['badges'] == null ? null : AnyValue.fromJson(json['badges']),
isRecommended = json['isRecommended'] == null ? null : nativeFromJson<bool>(json['isRecommended']),
bio = json['bio'] == null ? null : nativeFromJson<String>(json['bio']),
skills = json['skills'] == null ? null : AnyValue.fromJson(json['skills']),
industries = json['industries'] == null ? null : AnyValue.fromJson(json['industries']),
skills = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
industries = json['industries'] == null ? null : (json['industries'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
preferredLocations = json['preferredLocations'] == null ? null : (json['preferredLocations'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList(),
@@ -197,10 +201,10 @@ class ListStaffStaffs {
json['bio'] = nativeToJson<String?>(bio);
}
if (skills != null) {
json['skills'] = skills!.toJson();
json['skills'] = skills?.map((e) => nativeToJson<String>(e)).toList();
}
if (industries != null) {
json['industries'] = industries!.toJson();
json['industries'] = industries?.map((e) => nativeToJson<String>(e)).toList();
}
if (preferredLocations != null) {
json['preferredLocations'] = preferredLocations?.map((e) => nativeToJson<String>(e)).toList();

View File

@@ -16,8 +16,8 @@ class UpdateStaffVariablesBuilder {
Optional<int> _cancellationCount = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _reliabilityScore = Optional.optional(nativeFromJson, nativeToJson);
Optional<String> _bio = Optional.optional(nativeFromJson, nativeToJson);
Optional<AnyValue> _skills = Optional.optional(AnyValue.fromJson, defaultSerializer);
Optional<AnyValue> _industries = Optional.optional(AnyValue.fromJson, defaultSerializer);
Optional<List<String>> _skills = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<List<String>> _industries = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<List<String>> _preferredLocations = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
Optional<int> _maxDistanceMiles = Optional.optional(nativeFromJson, nativeToJson);
Optional<AnyValue> _languages = Optional.optional(AnyValue.fromJson, defaultSerializer);
@@ -93,11 +93,11 @@ class UpdateStaffVariablesBuilder {
_bio.value = t;
return this;
}
UpdateStaffVariablesBuilder skills(AnyValue? t) {
UpdateStaffVariablesBuilder skills(List<String>? t) {
_skills.value = t;
return this;
}
UpdateStaffVariablesBuilder industries(AnyValue? t) {
UpdateStaffVariablesBuilder industries(List<String>? t) {
_industries.value = t;
return this;
}
@@ -274,8 +274,8 @@ class UpdateStaffVariables {
late final Optional<int>cancellationCount;
late final Optional<int>reliabilityScore;
late final Optional<String>bio;
late final Optional<AnyValue>skills;
late final Optional<AnyValue>industries;
late final Optional<List<String>>skills;
late final Optional<List<String>>industries;
late final Optional<List<String>>preferredLocations;
late final Optional<int>maxDistanceMiles;
late final Optional<AnyValue>languages;
@@ -357,12 +357,16 @@ class UpdateStaffVariables {
bio.value = json['bio'] == null ? null : nativeFromJson<String>(json['bio']);
skills = Optional.optional(AnyValue.fromJson, defaultSerializer);
skills.value = json['skills'] == null ? null : AnyValue.fromJson(json['skills']);
skills = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
skills.value = json['skills'] == null ? null : (json['skills'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList();
industries = Optional.optional(AnyValue.fromJson, defaultSerializer);
industries.value = json['industries'] == null ? null : AnyValue.fromJson(json['industries']);
industries = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));
industries.value = json['industries'] == null ? null : (json['industries'] as List<dynamic>)
.map((e) => nativeFromJson<String>(e))
.toList();
preferredLocations = Optional.optional(listDeserializer(nativeFromJson), listSerializer(nativeToJson));

View File

@@ -51,7 +51,8 @@ class ProfileRepositoryMock {
const EmergencyContact(
name: 'Jane Doe',
phone: '555-987-6543',
relationship: 'Family',
relationship: RelationshipType.spouse,
id: 'contact_1',
),
];
}

View File

@@ -52,6 +52,8 @@ export 'src/entities/financial/staff_payment.dart';
// Profile
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';
@@ -77,3 +79,9 @@ export 'src/entities/home/reorder_item.dart';
// Availability
export 'src/entities/availability/availability_slot.dart';
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';
export 'src/adapters/profile/bank_account_adapter.dart';

View File

@@ -0,0 +1,53 @@
import '../../entities/profile/bank_account.dart';
/// Adapter for [BankAccount] to map data layer values to domain entity.
class BankAccountAdapter {
/// Maps primitive values to [BankAccount].
static BankAccount fromPrimitives({
required String id,
required String userId,
required String bankName,
required String? type,
String? accountNumber,
String? last4,
String? sortCode,
bool? isPrimary,
}) {
return BankAccount(
id: id,
userId: userId,
bankName: bankName,
accountNumber: accountNumber ?? '',
accountName: '', // Not provided by backend
last4: last4,
sortCode: sortCode,
type: _stringToType(type),
isPrimary: isPrimary ?? false,
);
}
static BankAccountType _stringToType(String? value) {
if (value == null) return BankAccountType.checking;
try {
// Assuming backend enum names match or are uppercase
return BankAccountType.values.firstWhere(
(e) => e.name.toLowerCase() == value.toLowerCase(),
orElse: () => BankAccountType.other,
);
} catch (_) {
return BankAccountType.other;
}
}
/// Converts domain type to string for backend.
static String typeToString(BankAccountType type) {
switch (type) {
case BankAccountType.checking:
return 'CHECKING';
case BankAccountType.savings:
return 'SAVINGS';
default:
return 'CHECKING';
}
}
}

View File

@@ -0,0 +1,19 @@
import '../../entities/profile/emergency_contact.dart';
/// Adapter for [EmergencyContact] to map data layer values to domain entity.
class EmergencyContactAdapter {
/// Maps primitive values to [EmergencyContact].
static EmergencyContact fromPrimitives({
required String id,
required String name,
required String phone,
String? relationship,
}) {
return EmergencyContact(
id: id,
name: name,
phone: phone,
relationship: EmergencyContact.stringToRelationshipType(relationship),
);
}
}

View File

@@ -0,0 +1,18 @@
/// Adapter for Experience data (skills/industries) to map data layer values to domain models.
class ExperienceAdapter {
/// Converts a dynamic list (from backend AnyValue) to List<String>.
///
/// Handles nulls and converts elements to Strings.
static List<String> fromDynamicList(dynamic data) {
if (data == null) return <String>[];
if (data is List) {
return data
.where((dynamic e) => e != null)
.map((dynamic e) => e.toString())
.toList();
}
return <String>[];
}
}

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart';
import 'relationship_type.dart';
/// Represents an emergency contact for a user.
///
@@ -6,19 +7,69 @@ import 'package:equatable/equatable.dart';
class EmergencyContact extends Equatable {
const EmergencyContact({
required this.id,
required this.name,
required this.relationship,
required this.phone,
});
/// Unique identifier.
final String id;
/// Full name of the contact.
final String name;
/// Relationship to the user (e.g. "Spouse", "Parent").
final String relationship;
final RelationshipType relationship;
/// Phone number.
final String phone;
@override
List<Object?> get props => <Object?>[name, relationship, phone];
}
List<Object?> get props => <Object?>[id, name, relationship, phone];
/// Returns a copy of this [EmergencyContact] with the given fields replaced.
EmergencyContact copyWith({
String? id,
String? name,
String? phone,
RelationshipType? relationship,
}) {
return EmergencyContact(
id: id ?? this.id,
name: name ?? this.name,
phone: phone ?? this.phone,
relationship: relationship ?? this.relationship,
);
}
/// Returns an empty [EmergencyContact].
static EmergencyContact empty() {
return const EmergencyContact(
id: '',
name: '',
phone: '',
relationship: RelationshipType.family,
);
}
/// Converts a string value to a [RelationshipType].
static RelationshipType stringToRelationshipType(String? value) {
if (value != null) {
final String strVal = value.toUpperCase();
switch (strVal) {
case 'FAMILY':
return RelationshipType.family;
case 'SPOUSE':
return RelationshipType.spouse;
case 'FRIEND':
return RelationshipType.friend;
case 'OTHER':
return RelationshipType.other;
default:
return RelationshipType.other;
}
}
return RelationshipType.other;
}
}

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

@@ -0,0 +1,6 @@
enum RelationshipType {
family,
spouse,
friend,
other,
}

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: