feat: implement emergency contact management with Firebase integration and relationship type handling

This commit is contained in:
Achintha Isuru
2026-01-27 12:27:22 -05:00
parent 536b020c52
commit 450683c45c
17 changed files with 226 additions and 81 deletions

View File

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

View File

@@ -6,7 +6,7 @@
/// Locales: 2 /// Locales: 2
/// Strings: 1026 (513 per locale) /// Strings: 1026 (513 per locale)
/// ///
/// Built on 2026-01-27 at 00:15 UTC /// Built on 2026-01-27 at 16:42 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

View File

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

View File

@@ -52,6 +52,7 @@ export 'src/entities/financial/staff_payment.dart';
// Profile // Profile
export 'src/entities/profile/staff_document.dart'; export 'src/entities/profile/staff_document.dart';
export 'src/entities/profile/attire_item.dart'; export 'src/entities/profile/attire_item.dart';
export 'src/entities/profile/relationship_type.dart';
// Ratings & Penalties // Ratings & Penalties
export 'src/entities/ratings/staff_rating.dart'; export 'src/entities/ratings/staff_rating.dart';
@@ -77,3 +78,6 @@ export 'src/entities/home/reorder_item.dart';
// Availability // Availability
export 'src/entities/availability/availability_slot.dart'; export 'src/entities/availability/availability_slot.dart';
export 'src/entities/availability/day_availability.dart'; export 'src/entities/availability/day_availability.dart';
// Adapters
export 'src/adapters/profile/emergency_contact_adapter.dart';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,13 @@ import 'package:krow_domain/krow_domain.dart';
extension EmergencyContactExtensions on EmergencyContact { extension EmergencyContactExtensions on EmergencyContact {
/// returns a copy of this [EmergencyContact] with the given fields replaced. /// returns a copy of this [EmergencyContact] with the given fields replaced.
EmergencyContact copyWith({ EmergencyContact copyWith({
String? id,
String? name, String? name,
String? phone, String? phone,
String? relationship, RelationshipType? relationship,
}) { }) {
return EmergencyContact( return EmergencyContact(
id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
phone: phone ?? this.phone, phone: phone ?? this.phone,
relationship: relationship ?? this.relationship, relationship: relationship ?? this.relationship,
@@ -18,9 +20,11 @@ extension EmergencyContactExtensions on EmergencyContact {
/// Returns an empty [EmergencyContact]. /// Returns an empty [EmergencyContact].
static EmergencyContact empty() { static EmergencyContact empty() {
return const EmergencyContact( return const EmergencyContact(
id: '',
name: '', name: '',
phone: '', phone: '',
relationship: 'family', relationship: RelationshipType.family,
); );
} }
} }

View File

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

View File

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

View File

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

View File

@@ -81,12 +81,10 @@ class EmergencyContactBloc
extends Bloc<EmergencyContactEvent, EmergencyContactState> { extends Bloc<EmergencyContactEvent, EmergencyContactState> {
final GetEmergencyContactsUseCase getEmergencyContacts; final GetEmergencyContactsUseCase getEmergencyContacts;
final SaveEmergencyContactsUseCase saveEmergencyContacts; final SaveEmergencyContactsUseCase saveEmergencyContacts;
final String staffId;
EmergencyContactBloc({ EmergencyContactBloc({
required this.getEmergencyContacts, required this.getEmergencyContacts,
required this.saveEmergencyContacts, required this.saveEmergencyContacts,
required this.staffId,
}) : super(const EmergencyContactState()) { }) : super(const EmergencyContactState()) {
on<EmergencyContactsLoaded>(_onLoaded); on<EmergencyContactsLoaded>(_onLoaded);
on<EmergencyContactAdded>(_onAdded); on<EmergencyContactAdded>(_onAdded);
@@ -102,13 +100,13 @@ class EmergencyContactBloc
emit(state.copyWith(status: EmergencyContactStatus.loading)); emit(state.copyWith(status: EmergencyContactStatus.loading));
try { try {
final contacts = await getEmergencyContacts( final contacts = await getEmergencyContacts(
GetEmergencyContactsArguments(staffId: staffId), const GetEmergencyContactsArguments(),
); );
emit(state.copyWith( emit(state.copyWith(
status: EmergencyContactStatus.success, status: EmergencyContactStatus.success,
contacts: contacts.isNotEmpty contacts: contacts.isNotEmpty
? contacts ? contacts
: [const EmergencyContact(name: '', phone: '', relationship: 'family')], : [EmergencyContact.empty()],
)); ));
} catch (e) { } catch (e) {
emit(state.copyWith( emit(state.copyWith(
@@ -123,7 +121,7 @@ class EmergencyContactBloc
Emitter<EmergencyContactState> emit, Emitter<EmergencyContactState> emit,
) { ) {
final updatedContacts = List<EmergencyContact>.from(state.contacts) final updatedContacts = List<EmergencyContact>.from(state.contacts)
..add(const EmergencyContact(name: '', phone: '', relationship: 'family')); ..add(EmergencyContact.empty());
emit(state.copyWith(contacts: updatedContacts)); emit(state.copyWith(contacts: updatedContacts));
} }
@@ -153,7 +151,6 @@ class EmergencyContactBloc
try { try {
await saveEmergencyContacts( await saveEmergencyContacts(
SaveEmergencyContactsArguments( SaveEmergencyContactsArguments(
staffId: staffId,
contacts: state.contacts, contacts: state.contacts,
), ),
); );

View File

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

View File

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