medora-provider/lib/screens/patientScreens/registrationScreens/patient_registration_screen.dart
DhanshCOSQ 66c3b2fb9c Patient registration complete with authentication flow fixed (#3)
Patient registration complete with authentication flow fixed

Co-authored-by: Benoy Bose <benoybose@gmail.com>
Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com>
Reviewed-on: cosqnet/telemednet#3
Reviewed-by: Benoy Bose <benoybose@cosq.net>
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
2024-10-31 14:20:35 +00:00

685 lines
24 KiB
Dart

// ignore_for_file: dead_code
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:telemednet/route_names.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import '../../../controller/patient_controller.dart';
import '../../../widgets/alert_screen.dart';
class PatientRegistrationScreen extends StatefulWidget {
const PatientRegistrationScreen({super.key});
@override
State<PatientRegistrationScreen> createState() =>
_PatientRegistrationScreenState();
}
class _PatientRegistrationScreenState extends State<PatientRegistrationScreen> {
final PatientController _controller = PatientController();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
bool _hasErrors = false;
final Map<String, String> _errors = {};
String? _gender;
DateTime? _dateOfBirth;
File? _image;
final ImagePicker _picker = ImagePicker();
String _selectedCountryCode = '+1';
final List<String> _countryCodes = ['+1', '+91', '+44', '+61', '+81'];
@override
void initState() {
super.initState();
_nameController.text = _controller.model.name ?? '';
if (_controller.model.phoneNumber != null) {
String phoneNumber = _controller.model.phoneNumber!;
if (phoneNumber.startsWith('+')) {
for (String code in _countryCodes) {
if (phoneNumber.startsWith(code)) {
_selectedCountryCode = code;
_phoneController.text = phoneNumber.substring(code.length);
break;
}
}
} else {
_phoneController.text = phoneNumber;
}
}
_gender = _controller.model.gender;
_dateOfBirth = _controller.model.dateOfBirth;
if (_controller.model.profileImagePath != null) {
_image = File(_controller.model.profileImagePath!);
}
_updateCombinedPhoneNumber(_phoneController.text);
}
Future<void> _getImage(ImageSource source) async {
final XFile? pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});
_controller.updateProfileImage(pickedFile.path);
}
}
void _updateCombinedPhoneNumber(String phoneNumber) {
String cleanPhoneNumber = phoneNumber.replaceAll(RegExp(r'^\+\d{1,3}'), '');
String fullPhoneNumber = '$_selectedCountryCode$cleanPhoneNumber';
_controller.updatePhoneNumber(fullPhoneNumber);
}
void _showImageSourceActionSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
'Select Image Source',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.photo_library, color: Colors.blue),
),
title: const Text('Choose from Gallery'),
onTap: () {
_getImage(ImageSource.gallery);
Navigator.pop(context);
},
),
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.photo_camera, color: Colors.blue),
),
title: const Text('Take a Photo'),
onTap: () {
_getImage(ImageSource.camera);
Navigator.pop(context);
},
),
const SizedBox(height: 16),
],
),
),
);
},
);
}
void _showResultDialog(bool isSuccess) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AlertScreen(
arguments: AlertArguments(
title: isSuccess ? 'Thank You' : 'Oops!',
message: isSuccess
? 'Profile created successfully!'
: 'Failed to create profile. Please try again.',
actionTitle: isSuccess ? 'Go to Dashboard' : 'Try Again',
type: isSuccess ? AlertType.success : AlertType.error,
onActionPressed: () {
Navigator.pop(context);
if (isSuccess) {
Navigator.pushReplacementNamed(
context,
RouteNames.patientDashboardScreen,
);
}
},
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
title: const Text(
'Create Profile',
style: TextStyle(color: Colors.black),
),
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
onPressed: () {
if (_validateAllFields()) {
_controller.savePatientData();
_showResultDialog(true);
} else {
_showValidationErrors();
}
},
icon: const Icon(Icons.check, color: Colors.blue, weight: 50),
),
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
GestureDetector(
onTap: () => _showImageSourceActionSheet(context),
child: Stack(
alignment: Alignment.bottomRight,
children: [
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.5),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
shape: BoxShape.circle,
border: Border.all(color: Colors.blue, width: 2),
),
child: CircleAvatar(
radius: 75,
backgroundImage:
_image != null ? FileImage(_image!) : null,
child: _image == null
? const Icon(Icons.person,
size: 50, color: Colors.blue)
: null,
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.5),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.camera_alt,
size: 20, color: Colors.white),
),
],
),
),
],
),
),
const SizedBox(height: 8),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.5),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildUniformField(
label: 'Name',
icon: Icons.person_outline,
child: TextField(
controller: _nameController,
onChanged: (value) => _controller.updateName(value),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter your name',
),
),
),
_buildUniformField(
label: 'Phone Number',
icon: Icons.phone_outlined,
child: Row(
children: [
DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedCountryCode,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedCountryCode = newValue;
});
_updateCombinedPhoneNumber(
_phoneController.text);
}
},
items:
_countryCodes.map<DropdownMenuItem<String>>(
(String code) {
return DropdownMenuItem<String>(
value: code,
child: Text(code),
);
},
).toList(),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _phoneController,
onChanged: (value) {
_updateCombinedPhoneNumber(value);
},
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter your phone number',
),
),
),
],
),
),
_buildUniformField(
label: 'Gender',
icon: Icons.people_outline,
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _gender,
isExpanded: true,
hint: const Text('Select gender'),
onChanged: (value) {
setState(() => _gender = value);
_controller.updateGender(value!);
},
items: ['Male', 'Female', 'Other']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
),
_buildUniformField(
label: 'Date of Birth',
icon: Icons.calendar_today_outlined,
child: InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _dateOfBirth ??
DateTime.now()
.subtract(const Duration(days: 365 * 18)),
firstDate: DateTime(1900),
lastDate: DateTime.now()
.subtract(const Duration(days: 365 * 18)),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Colors.blue),
),
child: child!,
);
},
);
if (picked != null && picked != _dateOfBirth) {
setState(() => _dateOfBirth = picked);
_controller.updateDateOfBirth(picked);
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
_dateOfBirth != null
? DateFormat('dd/MM/yyyy').format(_dateOfBirth!)
: 'Select date of birth',
style: TextStyle(
color: _dateOfBirth != null
? Colors.black87
: Colors.grey,
),
),
),
),
),
],
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.5),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildNavigationField(
'Address',
Icons.location_on,
() async {
final result = await Navigator.pushNamed(
context,
RouteNames.patientAdressScreen,
arguments: _controller,
);
if (result == true) {
setState(() {});
}
},
),
const Divider(height: 1),
_buildNavigationField(
'Family Members',
Icons.family_restroom_outlined,
() => Navigator.pushNamed(
context,
RouteNames.patientFamilyMembersScreen,
arguments: _controller,
),
),
],
)),
],
),
),
);
}
Widget _buildUniformField({
required String label,
required IconData icon,
required Widget child,
String? errorKey,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _errors.containsKey(errorKey ?? '')
? Colors.red
: Colors.grey.shade200,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _errors.containsKey(errorKey ?? '')
? Colors.red
: Colors.grey[600],
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
child: Row(
children: [
Icon(
icon,
size: 20,
color: _errors.containsKey(errorKey ?? '')
? Colors.red
: Colors.blue,
),
const SizedBox(width: 12),
Expanded(child: child),
],
),
),
if (_errors.containsKey(errorKey ?? ''))
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
child: Text(
_errors[errorKey]!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
),
);
}
bool _validateAllFields() {
setState(() {
_errors.clear();
_hasErrors = false;
final name = _nameController.text.trim();
if (name.isEmpty) {
_errors['name'] = 'Name is required';
_hasErrors = true;
} else if (name.length < 2 &&
RegExp(r'^[A-Za-z]+([.\s]?[A-Za-z]+)*$').hasMatch(name)) {
_errors['name'] = 'Name must be at least 2 characters';
_hasErrors = true;
}
final phoneNumber = _phoneController.text.trim();
if (phoneNumber.isEmpty) {
_errors['phone'] = 'Phone number is required';
_hasErrors = true;
} else if (!RegExp(r'^\d{10}$').hasMatch(phoneNumber)) {
_errors['phone'] = 'Enter a valid 10-digit phone number';
_hasErrors = true;
}
if (_gender == null) {
_errors['gender'] = 'Please select a gender';
_hasErrors = true;
}
if (_dateOfBirth == null) {
_errors['dob'] = 'Date of Birth is required';
_hasErrors = true;
} else {
final age = DateTime.now().difference(_dateOfBirth!).inDays ~/ 365;
if (age < 18) {
_errors['dob'] = 'User must be at least 18 years old';
_hasErrors = true;
}
}
if (_image == null) {
_errors['image'] = 'Profile picture is required';
_hasErrors = true;
}
// Validate required address fields
final address = _controller.model.address;
if (address.houseNo?.isEmpty ?? true) {
_errors['address'] = 'Please complete all required address fields';
_hasErrors = true;
}
// Validate address type and other label
if (address.addressType == 'Other' &&
(address.otherLabel?.isEmpty ?? true)) {
_errors['address'] = 'Please specify other address label';
_hasErrors = true;
}
});
return !_hasErrors;
}
void _showValidationErrors() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Validation Errors'),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: _errors.entries
.map((error) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
'${error.value}',
style: const TextStyle(color: Colors.red),
),
))
.toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
Widget _buildNavigationField(
String label, IconData icon, VoidCallback onTap) {
bool isAddressField = label == 'Address';
bool hasAddressError = _errors.containsKey('address');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: (isAddressField && hasAddressError)
? Colors.red.withOpacity(0.1)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon,
color: (isAddressField && hasAddressError)
? Colors.red
: Colors.blue,
size: 24),
),
title: Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: (isAddressField && hasAddressError)
? Colors.red
: Colors.black,
),
),
subtitle: isAddressField ? _buildAddressSubtitle() : null,
trailing: Icon(
Icons.chevron_right,
color:
(isAddressField && hasAddressError) ? Colors.red : Colors.blue,
),
onTap: onTap,
),
if (isAddressField && hasAddressError)
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
child: Text(
_errors['address']!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
);
}
Widget _buildAddressSubtitle() {
final address = _controller.model.address;
if (address.houseNo == null ||
address.line == null ||
address.city == null) {
return const Text(
'No address added',
style: TextStyle(color: Colors.grey),
);
}
return Text(
'${address.houseNo}, ${address.line}\n'
'${address.city}, ${address.state} ${address.pincode}\n'
'${address.addressType}${address.addressType == "Other" ? ": ${address.otherLabel}" : ""}',
style: const TextStyle(color: Colors.black87),
);
}
}