feat: implement emergency contact management with Firebase integration and relationship type handling
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,7 +51,8 @@ class ProfileRepositoryMock {
|
||||
const EmergencyContact(
|
||||
name: 'Jane Doe',
|
||||
phone: '555-987-6543',
|
||||
relationship: 'Family',
|
||||
relationship: RelationshipType.spouse,
|
||||
id: 'contact_1',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
enum RelationshipType {
|
||||
family,
|
||||
spouse,
|
||||
friend,
|
||||
other,
|
||||
}
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ class GetEmergencyContactsUseCase
|
||||
|
||||
@override
|
||||
Future<List<EmergencyContact>> call(GetEmergencyContactsArguments params) {
|
||||
return _repository.getContacts(params.staffId);
|
||||
return _repository.getContacts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ class SaveEmergencyContactsUseCase
|
||||
|
||||
@override
|
||||
Future<void> call(SaveEmergencyContactsArguments params) {
|
||||
return _repository.saveContacts(params.staffId, params.contacts);
|
||||
return _repository.saveContacts(params.contacts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user