feat: Implement emergency contact management feature with UI and BLoC integration
This commit is contained in:
@@ -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<List<EmergencyContact>> 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<void> saveEmergencyContacts(
|
||||
String staffId,
|
||||
List<EmergencyContact> contacts,
|
||||
) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
// Simulate save
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
||||
@@ -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<List<EmergencyContact>> getContacts(String staffId) {
|
||||
return _profileRepository.getEmergencyContacts(staffId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts) {
|
||||
return _profileRepository.saveEmergencyContacts(staffId, contacts);
|
||||
}
|
||||
}
|
||||
@@ -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<Object?> get props => [staffId];
|
||||
}
|
||||
@@ -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<EmergencyContact> contacts;
|
||||
|
||||
/// Creates a [SaveEmergencyContactsArguments].
|
||||
const SaveEmergencyContactsArguments({
|
||||
required this.staffId,
|
||||
required this.contacts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [staffId, contacts];
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<List<EmergencyContact>> getContacts(String staffId);
|
||||
|
||||
/// Saves the list of emergency contacts.
|
||||
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts);
|
||||
}
|
||||
@@ -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<GetEmergencyContactsArguments, List<EmergencyContact>> {
|
||||
final EmergencyContactRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [GetEmergencyContactsUseCase].
|
||||
GetEmergencyContactsUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<List<EmergencyContact>> call(GetEmergencyContactsArguments params) {
|
||||
return _repository.getContacts(params.staffId);
|
||||
}
|
||||
}
|
||||
@@ -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<SaveEmergencyContactsArguments, void> {
|
||||
final EmergencyContactRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [SaveEmergencyContactsUseCase].
|
||||
SaveEmergencyContactsUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<void> call(SaveEmergencyContactsArguments params) {
|
||||
return _repository.saveContacts(params.staffId, params.contacts);
|
||||
}
|
||||
}
|
||||
@@ -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<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];
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Future<void> _onLoaded(
|
||||
EmergencyContactsLoaded event,
|
||||
Emitter<EmergencyContactState> 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<EmergencyContactState> emit,
|
||||
) {
|
||||
final updatedContacts = List<EmergencyContact>.from(state.contacts)
|
||||
..add(const EmergencyContact(name: '', phone: '', relationship: 'family'));
|
||||
emit(state.copyWith(contacts: updatedContacts));
|
||||
}
|
||||
|
||||
void _onRemoved(
|
||||
EmergencyContactRemoved event,
|
||||
Emitter<EmergencyContactState> emit,
|
||||
) {
|
||||
final updatedContacts = List<EmergencyContact>.from(state.contacts)
|
||||
..removeAt(event.index);
|
||||
emit(state.copyWith(contacts: updatedContacts));
|
||||
}
|
||||
|
||||
void _onUpdated(
|
||||
EmergencyContactUpdated event,
|
||||
Emitter<EmergencyContactState> emit,
|
||||
) {
|
||||
final updatedContacts = List<EmergencyContact>.from(state.contacts);
|
||||
updatedContacts[event.index] = event.contact;
|
||||
emit(state.copyWith(contacts: updatedContacts));
|
||||
}
|
||||
|
||||
Future<void> _onSaved(
|
||||
EmergencyContactsSaved event,
|
||||
Emitter<EmergencyContactState> 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EmergencyContactBloc>()..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<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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
EmergencyContactSaveButton(state: state),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<EmergencyContactBloc>().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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<EmergencyContactBloc>().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<EmergencyContactBloc>().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<EmergencyContactBloc>().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<EmergencyContactBloc>()
|
||||
.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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>(ProfileRepositoryMock.new);
|
||||
i.addLazySingleton<EmergencyContactRepositoryInterface>(
|
||||
() => EmergencyContactRepositoryImpl(i.get<ProfileRepositoryMock>()),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton<GetEmergencyContactsUseCase>(
|
||||
() => GetEmergencyContactsUseCase(i.get<EmergencyContactRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton<SaveEmergencyContactsUseCase>(
|
||||
() => SaveEmergencyContactsUseCase(i.get<EmergencyContactRepositoryInterface>()),
|
||||
);
|
||||
|
||||
// BLoC
|
||||
i.addLazySingleton<EmergencyContactBloc>(
|
||||
() => EmergencyContactBloc(
|
||||
getEmergencyContacts: i.get<GetEmergencyContactsUseCase>(),
|
||||
saveEmergencyContacts: i.get<SaveEmergencyContactsUseCase>(),
|
||||
staffId: 'mock-staff-id', // TODO: Get direct from auth state
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(
|
||||
'/',
|
||||
child: (_) => const EmergencyContactScreen(),
|
||||
transition: TransitionType.rightToLeft,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user