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

View File

@@ -6,7 +6,7 @@
/// Locales: 2
/// 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
// ignore_for_file: type=lint, unused_import

View File

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

View File

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

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 'relationship_type.dart';
/// Represents an emergency contact for a user.
///
@@ -6,19 +7,69 @@ import 'package:equatable/equatable.dart';
class EmergencyContact extends Equatable {
const EmergencyContact({
required this.id,
required this.name,
required this.relationship,
required this.phone,
});
/// Unique identifier.
final String id;
/// Full name of the contact.
final String name;
/// Relationship to the user (e.g. "Spouse", "Parent").
final String relationship;
final RelationshipType relationship;
/// Phone number.
final String phone;
@override
List<Object?> get props => <Object?>[name, relationship, phone];
}
List<Object?> get props => <Object?>[id, name, relationship, phone];
/// Returns a copy of this [EmergencyContact] with the given fields replaced.
EmergencyContact copyWith({
String? id,
String? name,
String? phone,
RelationshipType? relationship,
}) {
return EmergencyContact(
id: id ?? this.id,
name: name ?? this.name,
phone: phone ?? this.phone,
relationship: relationship ?? this.relationship,
);
}
/// Returns an empty [EmergencyContact].
static EmergencyContact empty() {
return const EmergencyContact(
id: '',
name: '',
phone: '',
relationship: RelationshipType.family,
);
}
/// Converts a string value to a [RelationshipType].
static RelationshipType stringToRelationshipType(String? value) {
if (value != null) {
final 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 '../../domain/repositories/emergency_contact_repository_interface.dart';
/// Implementation of [EmergencyContactRepositoryInterface].
///
/// This repository delegates data operations to the [ProfileRepositoryMock]
/// (or real implementation) from the `data_connect` package.
/// This repository delegates data operations to Firebase Data Connect.
class EmergencyContactRepositoryImpl
implements EmergencyContactRepositoryInterface {
final ProfileRepositoryMock _profileRepository;
final dc.ExampleConnector _dataConnect;
final FirebaseAuth _firebaseAuth;
/// Creates an [EmergencyContactRepositoryImpl].
EmergencyContactRepositoryImpl(this._profileRepository);
EmergencyContactRepositoryImpl({
required dc.ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
@override
Future<List<EmergencyContact>> getContacts(String staffId) {
return _profileRepository.getEmergencyContacts(staffId);
Future<String> _getStaffId() async {
final user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
}
return result.data.staffs.first.id;
}
@override
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts) {
return _profileRepository.saveEmergencyContacts(staffId, contacts);
Future<List<EmergencyContact>> getContacts() async {
final staffId = await _getStaffId();
final result =
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
return result.data.emergencyContacts.map((dto) {
return EmergencyContactAdapter.fromPrimitives(
id: dto.id,
name: dto.name,
phone: dto.phone,
relationship: dto.relationship.stringValue,
);
}).toList();
}
}
@override
Future<void> saveContacts(List<EmergencyContact> contacts) async {
final staffId = await _getStaffId();
// 1. Get existing to delete
final existingResult =
await _dataConnect.getEmergencyContactsByStaffId(staffId: staffId).execute();
final existingIds =
existingResult.data.emergencyContacts.map((e) => e.id).toList();
// 2. Delete all existing
await Future.wait(existingIds.map(
(id) => _dataConnect.deleteEmergencyContact(id: id).execute()));
// 3. Create new
await Future.wait(contacts.map((contact) {
dc.RelationshipType rel = dc.RelationshipType.OTHER;
switch(contact.relationship) {
case RelationshipType.family: rel = dc.RelationshipType.FAMILY; break;
case RelationshipType.spouse: rel = dc.RelationshipType.SPOUSE; break;
case RelationshipType.friend: rel = dc.RelationshipType.FRIEND; break;
case RelationshipType.other: rel = dc.RelationshipType.OTHER; break;
}
return _dataConnect
.createEmergencyContact(
name: contact.name,
phone: contact.phone,
relationship: rel,
staffId: staffId,
)
.execute();
}));
}
}

View File

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

View File

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

View File

@@ -4,11 +4,13 @@ import 'package:krow_domain/krow_domain.dart';
extension EmergencyContactExtensions on EmergencyContact {
/// returns a copy of this [EmergencyContact] with the given fields replaced.
EmergencyContact copyWith({
String? id,
String? name,
String? phone,
String? relationship,
RelationshipType? relationship,
}) {
return EmergencyContact(
id: id ?? this.id,
name: name ?? this.name,
phone: phone ?? this.phone,
relationship: relationship ?? this.relationship,
@@ -18,9 +20,11 @@ extension EmergencyContactExtensions on EmergencyContact {
/// Returns an empty [EmergencyContact].
static EmergencyContact empty() {
return const EmergencyContact(
id: '',
name: '',
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.
abstract class EmergencyContactRepositoryInterface {
/// Retrieves the list of emergency contacts.
Future<List<EmergencyContact>> getContacts(String staffId);
Future<List<EmergencyContact>> getContacts();
/// Saves the list of emergency contacts.
Future<void> saveContacts(String staffId, List<EmergencyContact> contacts);
Future<void> saveContacts(List<EmergencyContact> contacts);
}

View File

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

View File

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

View File

@@ -81,12 +81,10 @@ 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);
@@ -102,13 +100,13 @@ class EmergencyContactBloc
emit(state.copyWith(status: EmergencyContactStatus.loading));
try {
final contacts = await getEmergencyContacts(
GetEmergencyContactsArguments(staffId: staffId),
const GetEmergencyContactsArguments(),
);
emit(state.copyWith(
status: EmergencyContactStatus.success,
contacts: contacts.isNotEmpty
? contacts
: [const EmergencyContact(name: '', phone: '', relationship: 'family')],
: [EmergencyContact.empty()],
));
} catch (e) {
emit(state.copyWith(
@@ -123,7 +121,7 @@ class EmergencyContactBloc
Emitter<EmergencyContactState> emit,
) {
final updatedContacts = List<EmergencyContact>.from(state.contacts)
..add(const EmergencyContact(name: '', phone: '', relationship: 'family'));
..add(EmergencyContact.empty());
emit(state.copyWith(contacts: updatedContacts));
}
@@ -153,7 +151,6 @@ class EmergencyContactBloc
try {
await saveEmergencyContacts(
SaveEmergencyContactsArguments(
staffId: staffId,
contacts: state.contacts,
),
);

View File

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

View File

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