elta AI App - Implementing Emergency Contact Selection During Trip Check-in
Plan for Implementing Emergency Contact Selection During Trip Check-in
This plan outlines the necessary modifications to your Flutter application using AWS Amplify to allow users to select an emergency contact when checking into a trip.
Phase 1: Update Data Models
- Modify
EmergencyContact
Model (if necessary for querying by user):- File: Your GraphQL schema (e.g.,
amplify/backend/api/<YOUR_API_NAME>/schema.graphql
) for theEmergencyContact
model. - Purpose: Ensure you can efficiently query emergency contacts for the logged-in user. If you're not already storing a
userID
or relying on the implicitowner
field from@auth
rules in a way that's easily queryable by your application logic, consider adding an explicituserID
. - Action: If you modify the schema, run
amplify codegen models
in your terminal to update the Dart model files inlib/models/
.
- File: Your GraphQL schema (e.g.,
- Modify
CheckInLog
Model:- File: Your GraphQL schema for the
CheckInLog
model. - Purpose: Add a field to store the ID of the selected emergency contact for that specific check-in.
- File (Dart Model - after codegen):
lib/models/CheckInLog.dart
will be updated. - Action: Run
amplify codegen models
after updating the schema.
- File: Your GraphQL schema for the
Change (Conceptual in Dart - this will be auto-generated):
// lib/models/CheckInLog.dart (Illustrative - generated by Amplify)
// ... other imports
// import 'package:amplify_flutter/amplify_flutter.dart';
// part 'CheckInLog.g.dart'; // Make sure this line exists
// @JsonSerializable(explicitToJson: true) // For older Amplify versions, or if you customize
// @immutable // For older Amplify versions
// class CheckInLog extends Model {
// // ... existing fields ...
// final String? selectedEmergencyContactID; // New field
// const CheckInLog({
// // ... existing parameters ...
// this.selectedEmergencyContactID,
// });
// // ... fromJson, toJson, copyWith, etc. will be updated ...
// }
Example Schema Change:
type CheckInLog @model @auth(rules: [
{allow: owner, operations: [create, read, update, delete]}
]) {
id: ID!
tripID: ID! # Assuming this links to a Trip model
# trip: Trip @connection(fields: ["tripID"]) # Optional connection
checkInTime: AWSDateTime!
expectedCheckOutTime: AWSDateTime!
actualCheckOutTime: AWSDateTime
notes: String
# userID: ID! @index(name: "byUser", queryField: "checkInLogsByUserID") # Or rely on owner
selectedEmergencyContactID: ID # New field to store the ID of the EmergencyContact
# selectedEmergencyContact: EmergencyContact @connection(fields: ["selectedEmergencyContactID"]) # Optional: if you want a direct GraphQL link
}
Example Schema Change (add userID
if not present and relying on it for queries):
type EmergencyContact @model @auth(rules: [
{allow: owner, operations: [create, read, update, delete]}
]) {
id: ID!
name: String!
phone: String!
relationship: String
userID: ID @index(name: "byUser", queryField: "emergencyContactsByUserID") # Add this if you want to query by userID
# If you rely solely on 'owner' field, ensure your repository logic handles that.
}
Phase 2: Update Emergency Contact Feature (Data Access)
- Modify
EmergencyContactRepository
:- File:
lib/features/emergency_contact/data/emergency_contact_repository.dart
- Purpose: Ensure there's a method to fetch all emergency contacts for the current user.
- File:
- Update/Create Emergency Contact Provider:
- File:
lib/features/emergency_contact/providers/emergency_contact_providers.dart
- Purpose: Provide the list of user's emergency contacts to the UI.
- File:
Example (using Riverpod):
// lib/features/emergency_contact/providers/emergency_contact_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:YOUR_APP_NAME/models/ModelProvider.dart'; // Update YOUR_APP_NAME
import 'package:YOUR_APP_NAME/features/emergency_contact/data/emergency_contact_repository.dart';
import 'package:YOUR_APP_NAME/common/services/auth_service.dart'; // Assuming you have this
// Provider for the repository itself
final emergencyContactRepositoryProvider = Provider<EmergencyContactRepository>((ref) {
return EmergencyContactRepository(); // Instantiate with dependencies if any
});
// Provider to fetch the list of emergency contacts for the current user
final userEmergencyContactsProvider = FutureProvider<List<EmergencyContact>>((ref) async {
final authService = ref.watch(authServiceProvider); // Assuming authServiceProvider exists
final cognitoUser = await authService.getCurrentUser(); // Or however you get user details
if (cognitoUser == null) { // Or check if user is signed in
safePrint('User not authenticated, cannot fetch emergency contacts.');
return []; // Or throw an error appropriate for your app's UX
}
// Use cognitoUser.userId or cognitoUser.username depending on what you use as 'owner' or 'userID'
final String userId = cognitoUser.userId;
final repository = ref.watch(emergencyContactRepositoryProvider);
return repository.getEmergencyContactsForUser(userId);
});
Example Method:
// lib/features/emergency_contact/data/emergency_contact_repository.dart
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:YOUR_APP_NAME/models/ModelProvider.dart'; // Update YOUR_APP_NAME
class EmergencyContactRepository {
// Assuming you inject DataStore or use Amplify.DataStore directly
// Example: final _dataStore = Amplify.DataStore;
Future<List<EmergencyContact>> getEmergencyContactsForUser(String userId) async {
try {
// If you added a userID field and indexed it:
final contacts = await Amplify.DataStore.query(
EmergencyContact.classType,
where: EmergencyContact.USERID.eq(userId), // Or EmergencyContact.OWNER.eq(userId) if using owner field
);
// If contacts are not directly queryable by userID but are linked via another model (e.g. Profile),
// you'd first fetch that model, then access its contacts.
// For simplicity, this example assumes direct queryability.
return contacts;
} catch (e) {
safePrint('Error fetching emergency contacts for user $userId: $e');
throw AmplifyException('Failed to fetch emergency contacts', underlyingException: e.toString());
}
}
// ... other existing methods (save, delete, etc.)
}
Phase 3: Implement UI for Selecting Emergency Contact
- Identify/Create Check-in UI Screen/Widget:
- This will likely be a new screen or a bottom sheet presented when the user initiates a check-in. For example,
lib/features/checkin/ui/check_in_screen.dart
orlib/features/checkin/ui/widgets/check_in_form.dart
. - This UI will take other check-in details (like expected checkout time) and now also the emergency contact.
- This will likely be a new screen or a bottom sheet presented when the user initiates a check-in. For example,
- Add Emergency Contact Selector to Check-in Form:
- File: The check-in UI file identified above.
- Purpose: Allow the user to pick from their list of emergency contacts.
Example Widget (using DropdownButtonFormField
and Riverpod):
// In your CheckInScreen or CheckInForm widget (StatefulWidget or ConsumerStatefulWidget for Riverpod)
// ... imports ...
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:YOUR_APP_NAME/models/ModelProvider.dart';
// import 'package:YOUR_APP_NAME/features/emergency_contact/providers/emergency_contact_providers.dart';
// import 'package:YOUR_APP_NAME/features/emergency_contact/ui/screens/add_edit_emergency_contact_screen.dart'; // For navigation
class CheckInForm extends ConsumerStatefulWidget { // Or StatefulWidget
final String tripId; // Assuming tripId is passed to this form
const CheckInForm({super.key, required this.tripId});
@override
ConsumerState<CheckInForm> createState() => _CheckInFormState();
}
class _CheckInFormState extends ConsumerState<CheckInForm> {
final _formKey = GlobalKey<FormState>();
DateTime? _expectedCheckOutTime;
EmergencyContact? _selectedEmergencyContact;
String _notes = '';
// Method to show date picker, etc. for _expectedCheckOutTime
@override
Widget build(BuildContext context) {
final emergencyContactsAsyncValue = ref.watch(userEmergencyContactsProvider);
return Form(
key: _formKey,
child: ListView( // Use ListView for scrollability if form is long
padding: EdgeInsets.all(16.0),
children: <Widget>[
// ... other form fields (expected checkout time, notes) ...
Text('Select Emergency Contact:', style: Theme.of(context).textTheme.titleMedium),
SizedBox(height: 8),
emergencyContactsAsyncValue.when(
data: (contacts) {
if (contacts.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('No emergency contacts found.'),
SizedBox(height: 8),
ElevatedButton(
onPressed: () {
// Navigate to add emergency contact screen
// Example: Navigator.push(context, MaterialPageRoute(builder: (_) => AddEditEmergencyContactScreen()));
// Or using go_router: context.push(AppRoute.addEmergencyContact.path);
// Make sure to refresh/refetch contacts after adding one.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Navigation to add contact screen - TBD')),
);
},
child: Text('Add Emergency Contact'),
),
],
);
}
return DropdownButtonFormField<EmergencyContact>(
decoration: InputDecoration(
labelText: 'Emergency Contact',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
filled: true,
),
value: _selectedEmergencyContact,
hint: Text('Choose an emergency contact'),
isExpanded: true,
items: contacts.map((EmergencyContact contact) {
return DropdownMenuItem<EmergencyContact>(
value: contact,
child: Text('${contact.name} (${contact.phone})'),
);
}).toList(),
onChanged: (EmergencyContact? newValue) {
setState(() {
_selectedEmergencyContact = newValue;
});
},
validator: (value) {
if (value == null) {
return 'Please select an emergency contact.';
}
return null;
},
);
},
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) {
safePrint('Error loading emergency contacts in dropdown: $err');
return Text('Error: Could not load contacts. $err');
}
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitCheckIn,
child: Text('Start Check-in'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
textStyle: TextStyle(fontSize: 16),
),
),
],
),
);
}
void _submitCheckIn() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
if (_expectedCheckOutTime == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please select an expected checkout time.')),
);
return;
}
if (_selectedEmergencyContact == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please select an emergency contact.')),
);
return;
}
// Call your CheckInController or CheckInRepository here
// Example:
// final checkInController = ref.read(checkInControllerProvider.notifier);
// checkInController.performCheckIn(
// tripId: widget.tripId,
// expectedCheckOutTime: _expectedCheckOutTime!,
// selectedEmergencyContactId: _selectedEmergencyContact!.id,
// notes: _notes,
// );
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Check-in submitted with contact: ${_selectedEmergencyContact!.name}')),
);
}
}
}
Phase 4: Update Check-in Controller and Repository
- Modify
CheckInController
(if you have one):- File:
lib/features/checkin/controller/check_in_controller.dart
(create if it doesn't exist for check-in specific logic, or integrate intoTripController
). - Purpose: Handle the logic of creating a
CheckInLog
object with the selected emergency contact ID.
- File:
- Create/Modify
CheckInRepository
:- File:
lib/features/checkin/data/check_in_repository.dart
- Purpose: Save the
CheckInLog
to Amplify DataStore.
- File:
Example:
// lib/features/checkin/data/check_in_repository.dart
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:YOUR_APP_NAME/models/ModelProvider.dart';
class CheckInRepository {
Future<CheckInLog> saveCheckIn(CheckInLog checkInLog) async {
try {
await Amplify.DataStore.save(checkInLog);
safePrint('CheckInLog saved successfully: ${checkInLog.id}');
return checkInLog;
} catch (e) {
safePrint('Error saving CheckInLog: $e');
throw AmplifyException('Failed to save check-in log', underlyingException: e.toString());
}
}
// You might also need methods to fetch active check-ins, update them, etc.
}
Example (Riverpod StateNotifier):
// lib/features/checkin/controller/check_in_controller.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:YOUR_APP_NAME/models/ModelProvider.dart';
import 'package:YOUR_APP_NAME/features/checkin/data/check_in_repository.dart'; // Create this
import 'package:YOUR_APP_NAME/common/services/auth_service.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
// State for the controller
abstract class CheckInState {}
class CheckInInitial extends CheckInState {}
class CheckInLoading extends CheckInState {}
class CheckInSuccess extends CheckInState {
final CheckInLog checkInLog;
CheckInSuccess(this.checkInLog);
}
class CheckInFailure extends CheckInState {
final String error;
CheckInFailure(this.error);
}
// Provider for the CheckInRepository
final checkInRepositoryProvider = Provider<CheckInRepository>((ref) {
return CheckInRepository(); // Instantiate with Amplify.DataStore or other dependencies
});
// Provider for the CheckInController
final checkInControllerProvider = StateNotifierProvider<CheckInController, CheckInState>((ref) {
return CheckInController(ref);
});
class CheckInController extends StateNotifier<CheckInState> {
final Ref _ref;
CheckInController(this._ref) : super(CheckInInitial());
Future<void> performCheckIn({
required String tripId,
required DateTime expectedCheckOutTime,
required String selectedEmergencyContactId,
String? notes,
}) async {
state = CheckInLoading();
try {
final authService = _ref.read(authServiceProvider);
final currentUser = await authService.getCurrentUser();
if (currentUser == null) {
throw Exception('User not authenticated.');
}
// final String userId = currentUser.userId; // Or however you get the owner/userID
final newCheckInLog = CheckInLog(
tripID: tripId,
checkInTime: TemporalDateTime.now(), // Use TemporalDateTime for Amplify
expectedCheckOutTime: TemporalDateTime(expectedCheckOutTime),
selectedEmergencyContactID: selectedEmergencyContactId,
notes: notes,
// userID: userId, // If your CheckInLog model has userID and it's not auto-set by owner policy
);
final checkInRepository = _ref.read(checkInRepositoryProvider);
final savedLog = await checkInRepository.saveCheckIn(newCheckInLog);
state = CheckInSuccess(savedLog);
} catch (e) {
safePrint('Error performing check-in: $e');
state = CheckInFailure(e.toString());
}
}
}
Phase 5: Integration and Testing
- Trigger Check-in UI: From your trip details page (
lib/features/trip/ui/trip_page/trip_page.dart
or similar), add a button "Start Check-in" that navigates to your newCheckInScreen
or shows theCheckInForm
in a dialog/bottom sheet. - Pass
tripId
: Ensure thetripId
is passed to the check-in form/screen. - Handle State: Use the
CheckInController
's state to show loading indicators, success messages, or error messages in the UI after attempting to check in. - Test Thoroughly:
- Check-in with an emergency contact selected.
- Verify data is saved correctly in Amplify DataStore (you can check via Amplify Console).
- Test the flow if no emergency contacts are available.
- Test error handling (e.g., network issues during save).
- Test UI responsiveness and form validation.
This detailed plan should guide you through implementing the emergency contact selection feature. Remember to replace YOUR_APP_NAME
with your actual application name in import paths.