From c124111f46029e9b9aba12b226b7ae5532d8c75f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 24 Jan 2026 20:40:02 -0500 Subject: [PATCH] feat: Implement emergency contact management feature with UI and BLoC integration --- .../src/mocks/profile_repository_mock.dart | 23 +++ .../navigation/profile_navigator.dart | 2 +- .../src/presentation/temp_theme_support.dart | 13 -- .../profile/lib/src/staff_profile_module.dart | 2 + .../features/staff/profile/pubspec.yaml | 2 + .../emergency_contact/analysis_options.yaml | 5 + .../emergency_contact_repository_impl.dart | 25 +++ .../get_emergency_contacts_arguments.dart | 13 ++ .../save_emergency_contacts_arguments.dart | 20 ++ .../emergency_contact_extensions.dart | 26 +++ ...mergency_contact_repository_interface.dart | 13 ++ .../get_emergency_contacts_usecase.dart | 21 ++ .../save_emergency_contacts_usecase.dart | 20 ++ .../blocs/emergency_contact_bloc.dart | 168 ++++++++++++++++ .../pages/emergency_contact_screen.dart | 92 +++++++++ .../widgets/emergency_contact_add_button.dart | 34 ++++ .../widgets/emergency_contact_form_item.dart | 188 ++++++++++++++++++ .../emergency_contact_info_banner.dart | 21 ++ .../emergency_contact_save_button.dart | 57 ++++++ .../src/staff_emergency_contact_module.dart | 46 +++++ .../lib/staff_emergency_contact.dart | 5 + .../onboarding/emergency_contact/pubspec.yaml | 34 ++++ .../lib/src/staff_profile_info_module.dart | 1 - apps/mobile/pubspec.lock | 14 -- apps/mobile/pubspec.yaml | 3 + 25 files changed, 819 insertions(+), 29 deletions(-) delete mode 100644 apps/mobile/packages/features/staff/profile/lib/src/presentation/temp_theme_support.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/analysis_options.yaml create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/data/repositories/emergency_contact_repository_impl.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/get_emergency_contacts_arguments.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/save_emergency_contacts_arguments.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/extensions/emergency_contact_extensions.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/repositories/emergency_contact_repository_interface.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/get_emergency_contacts_usecase.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/save_emergency_contacts_usecase.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/blocs/emergency_contact_bloc.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_add_button.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_form_item.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_info_banner.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/staff_emergency_contact_module.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/staff_emergency_contact.dart create mode 100644 apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/pubspec.yaml diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart index b4409833..444e5363 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart @@ -42,4 +42,27 @@ class ProfileRepositoryMock { // Simulate processing delay await Future.delayed(const Duration(milliseconds: 300)); } + + /// Fetches emergency contacts for the given staff ID. + /// + /// Returns a list of [EmergencyContact]. + Future> getEmergencyContacts(String staffId) async { + await Future.delayed(const Duration(milliseconds: 500)); + return [ + const EmergencyContact( + name: 'Jane Doe', + phone: '555-987-6543', + relationship: 'Family', + ), + ]; + } + + /// Saves emergency contacts for the given staff ID. + Future saveEmergencyContacts( + String staffId, + List contacts, + ) async { + await Future.delayed(const Duration(seconds: 1)); + // Simulate save + } } diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart index adde9c58..30b71042 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart @@ -13,7 +13,7 @@ extension ProfileNavigator on IModularNavigator { /// Navigates to the emergency contact page. void pushEmergencyContact() { - pushNamed('/profile/onboarding/emergency-contact'); + pushNamed('./emergency-contact'); } /// Navigates to the experience page. diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/temp_theme_support.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/temp_theme_support.dart deleted file mode 100644 index 26115503..00000000 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/temp_theme_support.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -/// NOTE: This is a TEMPORARY class to allow the prototype code to compile -/// without immediate design system integration. -/// It will be replaced by the actual Design System tokens (UiColors) in Step 4. -class AppColors { - static const Color krowBackground = Color(0xFFF9F9F9); - static const Color krowBlue = Color(0xFF0055FF); - static const Color krowYellow = Color(0xFFFFCC00); - static const Color krowBorder = Color(0xFFE0E0E0); - static const Color krowCharcoal = Color(0xFF333333); - static const Color krowMuted = Color(0xFF808080); -} diff --git a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart index 64bba981..2cdbc0af 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/staff_profile_module.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:staff_profile_info/staff_profile_info.dart'; +import 'package:staff_emergency_contact/staff_emergency_contact.dart'; import 'data/repositories/profile_repository_impl.dart'; import 'domain/repositories/profile_repository.dart'; @@ -53,5 +54,6 @@ class StaffProfileModule extends Module { void routes(RouteManager r) { r.child('/', child: (BuildContext context) => const StaffProfilePage()); r.module('/onboarding', module: StaffProfileInfoModule()); + r.module('/emergency-contact', module: StaffEmergencyContactModule()); } } diff --git a/apps/mobile/packages/features/staff/profile/pubspec.yaml b/apps/mobile/packages/features/staff/profile/pubspec.yaml index 6f72c239..5f494249 100644 --- a/apps/mobile/packages/features/staff/profile/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile/pubspec.yaml @@ -32,6 +32,8 @@ dependencies: # Feature Packages staff_profile_info: path: ../profile_sections/onboarding/profile_info + staff_emergency_contact: + path: ../profile_sections/onboarding/emergency_contact dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/analysis_options.yaml b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/analysis_options.yaml new file mode 100644 index 00000000..5dfc2bd0 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/analysis_options.yaml @@ -0,0 +1,5 @@ +# include: package:flutter_lints/flutter.yaml + +linter: + rules: + public_member_api_docs: false diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/data/repositories/emergency_contact_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/data/repositories/emergency_contact_repository_impl.dart new file mode 100644 index 00000000..a69b4bf7 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/data/repositories/emergency_contact_repository_impl.dart @@ -0,0 +1,25 @@ +import 'package:krow_data_connect/krow_data_connect.dart'; +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. +class EmergencyContactRepositoryImpl + implements EmergencyContactRepositoryInterface { + final ProfileRepositoryMock _profileRepository; + + /// Creates an [EmergencyContactRepositoryImpl]. + EmergencyContactRepositoryImpl(this._profileRepository); + + @override + Future> getContacts(String staffId) { + return _profileRepository.getEmergencyContacts(staffId); + } + + @override + Future saveContacts(String staffId, List contacts) { + return _profileRepository.saveEmergencyContacts(staffId, contacts); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/get_emergency_contacts_arguments.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/get_emergency_contacts_arguments.dart new file mode 100644 index 00000000..8fe22839 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/get_emergency_contacts_arguments.dart @@ -0,0 +1,13 @@ +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}); + + @override + List get props => [staffId]; +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/save_emergency_contacts_arguments.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/save_emergency_contacts_arguments.dart new file mode 100644 index 00000000..2aa195b5 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/arguments/save_emergency_contacts_arguments.dart @@ -0,0 +1,20 @@ +import 'package:krow_core/core.dart'; +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 contacts; + + /// Creates a [SaveEmergencyContactsArguments]. + const SaveEmergencyContactsArguments({ + required this.staffId, + required this.contacts, + }); + + @override + List get props => [staffId, contacts]; +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/extensions/emergency_contact_extensions.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/extensions/emergency_contact_extensions.dart new file mode 100644 index 00000000..d246a6c2 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/extensions/emergency_contact_extensions.dart @@ -0,0 +1,26 @@ +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', + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/repositories/emergency_contact_repository_interface.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/repositories/emergency_contact_repository_interface.dart new file mode 100644 index 00000000..3cd5792b --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/repositories/emergency_contact_repository_interface.dart @@ -0,0 +1,13 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Repository interface for managing emergency contacts. +/// +/// This interface defines the contract for fetching and saving emergency contact information. +/// It must be implemented by the data layer. +abstract class EmergencyContactRepositoryInterface { + /// Retrieves the list of emergency contacts. + Future> getContacts(String staffId); + + /// Saves the list of emergency contacts. + Future saveContacts(String staffId, List contacts); +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/get_emergency_contacts_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/get_emergency_contacts_usecase.dart new file mode 100644 index 00000000..5c4faa1b --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/get_emergency_contacts_usecase.dart @@ -0,0 +1,21 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/arguments/get_emergency_contacts_arguments.dart'; +import '../../domain/repositories/emergency_contact_repository_interface.dart'; + +/// Use case for retrieving emergency contacts. +/// +/// This use case encapsulates the business logic for fetching emergency contacts +/// for a specific staff member. +class GetEmergencyContactsUseCase + extends UseCase> { + final EmergencyContactRepositoryInterface _repository; + + /// Creates a [GetEmergencyContactsUseCase]. + GetEmergencyContactsUseCase(this._repository); + + @override + Future> call(GetEmergencyContactsArguments params) { + return _repository.getContacts(params.staffId); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/save_emergency_contacts_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/save_emergency_contacts_usecase.dart new file mode 100644 index 00000000..49563678 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/domain/usecases/save_emergency_contacts_usecase.dart @@ -0,0 +1,20 @@ +import 'package:krow_core/core.dart'; +import '../arguments/save_emergency_contacts_arguments.dart'; +import '../repositories/emergency_contact_repository_interface.dart'; + +/// Use case for saving emergency contacts. +/// +/// This use case encapsulates the business logic for saving emergency contacts +/// for a specific staff member. +class SaveEmergencyContactsUseCase + extends UseCase { + final EmergencyContactRepositoryInterface _repository; + + /// Creates a [SaveEmergencyContactsUseCase]. + SaveEmergencyContactsUseCase(this._repository); + + @override + Future call(SaveEmergencyContactsArguments params) { + return _repository.saveContacts(params.staffId, params.contacts); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/blocs/emergency_contact_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/blocs/emergency_contact_bloc.dart new file mode 100644 index 00000000..73478061 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/blocs/emergency_contact_bloc.dart @@ -0,0 +1,168 @@ +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'; + +// Events +abstract class EmergencyContactEvent extends Equatable { + const EmergencyContactEvent(); + + @override + List get props => []; +} + +class EmergencyContactsLoaded extends EmergencyContactEvent {} + +class EmergencyContactAdded extends EmergencyContactEvent {} + +class EmergencyContactRemoved extends EmergencyContactEvent { + final int index; + + const EmergencyContactRemoved(this.index); + + @override + List get props => [index]; +} + +class EmergencyContactUpdated extends EmergencyContactEvent { + final int index; + final EmergencyContact contact; + + const EmergencyContactUpdated(this.index, this.contact); + + @override + List 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 contacts; + final String? errorMessage; + + const EmergencyContactState({ + this.status = EmergencyContactStatus.initial, + this.contacts = const [], + this.errorMessage, + }); + + EmergencyContactState copyWith({ + EmergencyContactStatus? status, + List? 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 get props => [status, contacts, errorMessage]; +} + +// BLoC +class EmergencyContactBloc + extends Bloc { + final GetEmergencyContactsUseCase getEmergencyContacts; + final SaveEmergencyContactsUseCase saveEmergencyContacts; + final String staffId; + + EmergencyContactBloc({ + required this.getEmergencyContacts, + required this.saveEmergencyContacts, + required this.staffId, + }) : super(const EmergencyContactState()) { + on(_onLoaded); + on(_onAdded); + on(_onRemoved); + on(_onUpdated); + on(_onSaved); + } + + Future _onLoaded( + EmergencyContactsLoaded event, + Emitter emit, + ) async { + emit(state.copyWith(status: EmergencyContactStatus.loading)); + try { + final contacts = await getEmergencyContacts( + GetEmergencyContactsArguments(staffId: staffId), + ); + emit(state.copyWith( + status: EmergencyContactStatus.success, + contacts: contacts.isNotEmpty + ? contacts + : [const EmergencyContact(name: '', phone: '', relationship: 'family')], + )); + } catch (e) { + emit(state.copyWith( + status: EmergencyContactStatus.failure, + errorMessage: e.toString(), + )); + } + } + + void _onAdded( + EmergencyContactAdded event, + Emitter emit, + ) { + final updatedContacts = List.from(state.contacts) + ..add(const EmergencyContact(name: '', phone: '', relationship: 'family')); + emit(state.copyWith(contacts: updatedContacts)); + } + + void _onRemoved( + EmergencyContactRemoved event, + Emitter emit, + ) { + final updatedContacts = List.from(state.contacts) + ..removeAt(event.index); + emit(state.copyWith(contacts: updatedContacts)); + } + + void _onUpdated( + EmergencyContactUpdated event, + Emitter emit, + ) { + final updatedContacts = List.from(state.contacts); + updatedContacts[event.index] = event.contact; + emit(state.copyWith(contacts: updatedContacts)); + } + + Future _onSaved( + EmergencyContactsSaved event, + Emitter emit, + ) async { + emit(state.copyWith(status: EmergencyContactStatus.saving)); + try { + await saveEmergencyContacts( + SaveEmergencyContactsArguments( + staffId: staffId, + contacts: state.contacts, + ), + ); + emit(state.copyWith(status: EmergencyContactStatus.success)); + } catch (e) { + emit(state.copyWith( + status: EmergencyContactStatus.failure, + errorMessage: e.toString(), + )); + } + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart new file mode 100644 index 00000000..3d53be99 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/pages/emergency_contact_screen.dart @@ -0,0 +1,92 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import '../blocs/emergency_contact_bloc.dart'; +import '../widgets/emergency_contact_add_button.dart'; +import '../widgets/emergency_contact_form_item.dart'; +import '../widgets/emergency_contact_info_banner.dart'; +import '../widgets/emergency_contact_save_button.dart'; + + +/// The Staff Emergency Contact screen. +/// +/// This screen allows staff to manage their emergency contacts during onboarding. +/// It uses [EmergencyContactBloc] for state management and follows the +/// composed-widget pattern for UI elements. +class EmergencyContactScreen extends StatelessWidget { + const EmergencyContactScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => + Modular.get()..add(EmergencyContactsLoaded()), + child: const _EmergencyContactView(), + ); + } +} + +class _EmergencyContactView extends StatelessWidget { + const _EmergencyContactView(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + leading: IconButton( + icon: Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), + onPressed: () => Modular.to.pop(), + ), + title: Text( + 'Emergency Contact', + style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: UiColors.border, height: 1.0), + ), + ), + body: BlocConsumer( + 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), + ], + ), + ), + ), + EmergencyContactSaveButton(state: state), + ], + ); + }, + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_add_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_add_button.dart new file mode 100644 index 00000000..40f9b81e --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_add_button.dart @@ -0,0 +1,34 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/emergency_contact_bloc.dart'; + +class EmergencyContactAddButton extends StatelessWidget { + const EmergencyContactAddButton({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: TextButton.icon( + onPressed: () => + context.read().add(EmergencyContactAdded()), + icon: Icon(UiIcons.add, size: 20.0), + label: Text( + 'Add Another Contact', + style: UiTypography.title2b, + ), + style: TextButton.styleFrom( + foregroundColor: UiColors.primary, + padding: EdgeInsets.symmetric( + horizontal: UiConstants.space6, + vertical: UiConstants.space3, + ), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusFull, + side: BorderSide(color: UiColors.primary), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_form_item.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_form_item.dart new file mode 100644 index 00000000..b05c2783 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_form_item.dart @@ -0,0 +1,188 @@ +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 { + final int index; + final EmergencyContact contact; + final int totalContacts; + + const EmergencyContactFormItem({ + super.key, + required this.index, + required this.contact, + required this.totalContacts, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: UiConstants.space4), + padding: EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + SizedBox(height: UiConstants.space4), + _buildLabel('Full Name'), + _buildTextField( + initialValue: contact.name, + hint: 'Contact name', + icon: UiIcons.user, + onChanged: (val) => context.read().add( + EmergencyContactUpdated( + index, + contact.copyWith(name: val), + ), + ), + ), + SizedBox(height: UiConstants.space4), + _buildLabel('Phone Number'), + _buildTextField( + initialValue: contact.phone, + hint: '+1 (555) 000-0000', + icon: UiIcons.phone, + onChanged: (val) => context.read().add( + EmergencyContactUpdated( + index, + contact.copyWith(phone: val), + ), + ), + ), + SizedBox(height: UiConstants.space4), + _buildLabel('Relationship'), + _buildDropdown( + context, + value: contact.relationship, + items: const ['family', 'friend', 'partner', 'other'], + onChanged: (val) { + if (val != null) { + context.read().add( + EmergencyContactUpdated( + index, + contact.copyWith(relationship: val), + ), + ); + } + }, + ), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Contact ${index + 1}', + style: UiTypography.title2m.copyWith( + color: UiColors.textPrimary, + ), + ), + if (totalContacts > 1) + IconButton( + icon: Icon( + UiIcons.delete, + color: UiColors.textError, + size: 20.0, + ), + onPressed: () => context + .read() + .add(EmergencyContactRemoved(index)), + ), + ], + ); + } + + Widget _buildLabel(String label) { + return Padding( + padding: EdgeInsets.only(bottom: UiConstants.space2), + child: Text( + label, + style: UiTypography.body2m.copyWith( + color: UiColors.textSecondary, + ), + ), + ); + } + + Widget _buildTextField({ + required String initialValue, + required String hint, + required IconData icon, + required Function(String) onChanged, + }) { + return TextFormField( + initialValue: initialValue, + style: UiTypography.body1r.copyWith( + color: UiColors.textPrimary, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: UiColors.textPlaceholder), + prefixIcon: Icon(icon, color: UiColors.textSecondary, size: 20.0), + filled: true, + fillColor: UiColors.bgPopup, + contentPadding: EdgeInsets.symmetric(vertical: UiConstants.space4), + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: BorderSide(color: UiColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: BorderSide(color: UiColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: BorderSide(color: UiColors.primary), + ), + ), + onChanged: onChanged, + ); + } + + Widget _buildDropdown( + BuildContext context, { + required String value, + required List 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( + value: items.contains(value) ? value : items.first, + isExpanded: true, + icon: Icon(UiIcons.chevronDown, color: UiColors.textSecondary), + items: items.map((String item) { + return DropdownMenuItem( + value: item, + child: Text( + item.toUpperCase(), + style: UiTypography.body1r.copyWith( + color: UiColors.textPrimary, + ), + ), + ); + }).toList(), + onChanged: onChanged, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_info_banner.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_info_banner.dart new file mode 100644 index 00000000..975529be --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_info_banner.dart @@ -0,0 +1,21 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class EmergencyContactInfoBanner extends StatelessWidget { + const EmergencyContactInfoBanner({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.accent.withOpacity(0.2), + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Text( + 'Please provide at least one emergency contact. This information will only be used in case of an emergency during your shifts.', + style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart new file mode 100644 index 00000000..28c4db1a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/presentation/widgets/emergency_contact_save_button.dart @@ -0,0 +1,57 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/emergency_contact_bloc.dart'; + +class EmergencyContactSaveButton extends StatelessWidget { + final EmergencyContactState state; + + const EmergencyContactSaveButton({super.key, required this.state}); + + @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() + .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, + ), + elevation: 0, + ), + child: state.status == EmergencyContactStatus.saving + ? SizedBox( + height: 20.0, + width: 20.0, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(UiColors.primaryForeground), + ), + ) + : Text( + 'Save & Continue', + style: UiTypography.title2b, + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/staff_emergency_contact_module.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/staff_emergency_contact_module.dart new file mode 100644 index 00000000..66048891 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/src/staff_emergency_contact_module.dart @@ -0,0 +1,46 @@ +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'data/repositories/emergency_contact_repository_impl.dart'; +import 'domain/repositories/emergency_contact_repository_interface.dart'; +import 'domain/usecases/get_emergency_contacts_usecase.dart'; +import 'domain/usecases/save_emergency_contacts_usecase.dart'; +import 'presentation/blocs/emergency_contact_bloc.dart'; +import 'presentation/pages/emergency_contact_screen.dart'; + +class StaffEmergencyContactModule extends Module { + @override + void binds(Injector i) { + // Repository + // Uses ProfileRepositoryMock from data_connect + i.addLazySingleton(ProfileRepositoryMock.new); + i.addLazySingleton( + () => EmergencyContactRepositoryImpl(i.get()), + ); + + // UseCases + i.addLazySingleton( + () => GetEmergencyContactsUseCase(i.get()), + ); + i.addLazySingleton( + () => SaveEmergencyContactsUseCase(i.get()), + ); + + // BLoC + i.addLazySingleton( + () => EmergencyContactBloc( + getEmergencyContacts: i.get(), + saveEmergencyContacts: i.get(), + staffId: 'mock-staff-id', // TODO: Get direct from auth state + ), + ); + } + + @override + void routes(RouteManager r) { + r.child( + '/', + child: (_) => const EmergencyContactScreen(), + transition: TransitionType.rightToLeft, + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/staff_emergency_contact.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/staff_emergency_contact.dart new file mode 100644 index 00000000..8f364342 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/lib/staff_emergency_contact.dart @@ -0,0 +1,5 @@ +library staff_emergency_contact; + +export 'src/staff_emergency_contact_module.dart'; +export 'src/presentation/pages/emergency_contact_screen.dart'; +// Export other necessary classes if needed by consumers diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/pubspec.yaml new file mode 100644 index 00000000..15529c1b --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/emergency_contact/pubspec.yaml @@ -0,0 +1,34 @@ +name: staff_emergency_contact +description: Staff Emergency Contact feature. +version: 0.0.1 +publish_to: none +resolution: workspace + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_bloc: ^8.1.0 + flutter_modular: ^6.3.0 + equatable: ^2.0.5 + + # Architecture Packages + krow_domain: + path: ../../../../../domain + krow_core: + path: ../../../../../core + krow_data_connect: + path: ../../../../../data_connect + design_system: + path: ../../../../../design_system + core_localization: + path: ../../../../../core_localization + +dev_dependencies: + flutter_test: + sdk: flutter + bloc_test: ^9.1.0 + mocktail: ^1.0.0 diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart index 59c31ba7..5679ae1f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/staff_profile_info_module.dart @@ -57,6 +57,5 @@ class StaffProfileInfoModule extends Module { '/personal-info/', child: (BuildContext context) => const PersonalInfoPage(), ); - // Additional routes will be added as more onboarding pages are implemented } } diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index ed6afc89..21ca16ea 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -1064,20 +1064,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" - staff_profile: - dependency: transitive - description: - path: "packages/features/staff/profile" - relative: true - source: path - version: "0.0.1" - staff_profile_info: - dependency: transitive - description: - path: "packages/features/staff/profile_sections/onboarding/profile_info" - relative: true - source: path - version: "0.0.1" stream_channel: dependency: transitive description: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 7bf83636..646c5e3d 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -12,6 +12,9 @@ workspace: - packages/features/staff/authentication - packages/features/staff/home - packages/features/staff/staff_main + - packages/features/staff/profile + - packages/features/staff/profile_sections/onboarding/emergency_contact + - packages/features/staff/profile_sections/onboarding/profile_info - packages/features/client/authentication - packages/features/client/home - packages/features/client/settings