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
|
// Simulate processing delay
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
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.
|
/// Navigates to the emergency contact page.
|
||||||
void pushEmergencyContact() {
|
void pushEmergencyContact() {
|
||||||
pushNamed('/profile/onboarding/emergency-contact');
|
pushNamed('./emergency-contact');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the experience page.
|
/// 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:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:staff_profile_info/staff_profile_info.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 'data/repositories/profile_repository_impl.dart';
|
||||||
import 'domain/repositories/profile_repository.dart';
|
import 'domain/repositories/profile_repository.dart';
|
||||||
@@ -53,5 +54,6 @@ class StaffProfileModule extends Module {
|
|||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
||||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||||
|
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ dependencies:
|
|||||||
# Feature Packages
|
# Feature Packages
|
||||||
staff_profile_info:
|
staff_profile_info:
|
||||||
path: ../profile_sections/onboarding/profile_info
|
path: ../profile_sections/onboarding/profile_info
|
||||||
|
staff_emergency_contact:
|
||||||
|
path: ../profile_sections/onboarding/emergency_contact
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
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/',
|
'/personal-info/',
|
||||||
child: (BuildContext context) => const PersonalInfoPage(),
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ workspace:
|
|||||||
- packages/features/staff/authentication
|
- packages/features/staff/authentication
|
||||||
- packages/features/staff/home
|
- packages/features/staff/home
|
||||||
- packages/features/staff/staff_main
|
- 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/authentication
|
||||||
- packages/features/client/home
|
- packages/features/client/home
|
||||||
- packages/features/client/settings
|
- packages/features/client/settings
|
||||||
|
|||||||
Reference in New Issue
Block a user