Split the buisiness logic, to keep only doctor related buisiness

This commit is contained in:
Aswin B. S 2025-02-26 15:31:44 +05:30
parent 54f05e377d
commit 370107db72
26 changed files with 85 additions and 6159 deletions

View File

@ -1,104 +0,0 @@
import 'package:medora/data/models/patient.dart';
import '../data/services/patient_registration_service.dart';
class PatientController {
final PatientModel model = PatientModel();
Map<String, String> validationErrors = {};
void clearValidationErrors() {
validationErrors.clear();
}
void updateName(String name) {
model.name = name;
}
void updatePhoneNumber(String phoneNumber) {
model.phoneNumber = phoneNumber;
}
void updateGender(String gender) {
model.gender = gender;
}
void updateDateOfBirth(DateTime dateOfBirth) {
model.dateOfBirth = dateOfBirth;
}
void updateProfileImage(String imagePath) {
model.profileImagePath = imagePath;
}
void updateHouseNo(String houseNo) {
model.address.houseNo = houseNo;
}
void updateLine(String line) {
model.address.line = line;
}
void updateTown(String town) {
model.address.town = town;
}
void updatePincode(String pincode) {
model.address.pincode = pincode;
}
void updateCountry(String country) {
model.address.country = country;
}
void updateState(String state) {
model.address.state = state;
}
void updateCity(String city) {
model.address.city = city;
}
void updateAddressType(String addressType) {
model.address.addressType = addressType;
}
void updateOtherLabel(String otherLabel) {
model.address.otherLabel = otherLabel;
}
void addFamilyMember(FamilyMember member) {
model.familyMembers.add(member);
}
void updateFamilyMember(int index, FamilyMember member) {
if (index >= 0 && index < model.familyMembers.length) {
model.familyMembers[index] = member;
}
}
void deleteFamilyMember(int index) {
if (index >= 0 && index < model.familyMembers.length) {
model.familyMembers.removeAt(index);
}
}
Future<bool> savePatientData() async {
return await PatientProfileService.savePatientProfile(this);
}
Future<bool> loadPatientData() async {
PatientModel? loadedModel = await PatientProfileService.getPatientProfile();
if (loadedModel != null) {
model.updateFrom(loadedModel);
return true;
}
return false;
}
Future<bool> updatePatientData() async {
return await PatientProfileService.updatePatientProfile(model);
}
Future<bool> deletePatientData() async {
return await PatientProfileService.deletePatientProfile();
}
}

View File

@ -1,135 +0,0 @@
class PatientModel {
String? name;
String? phoneNumber;
String? gender;
DateTime? dateOfBirth;
String? profileImagePath;
String? profileImageUrl;
PatientAddress address;
List<FamilyMember> familyMembers = [];
PatientModel() : address = PatientAddress();
Map<String, dynamic> toJson() {
return {
'name': name,
'phoneNumber': phoneNumber,
'gender': gender,
'dateOfBirth': dateOfBirth?.toIso8601String(),
'profileImagePath': profileImagePath,
'profileImageUrl': profileImageUrl,
'address': address.toJson(),
'familyMembers': familyMembers.map((member) => member.toJson()).toList(),
};
}
PatientModel.fromJson(Map<String, dynamic> json)
: address = PatientAddress() {
name = json['name'];
phoneNumber = json['phoneNumber'];
gender = json['gender'];
dateOfBirth = json['dateOfBirth'] != null
? DateTime.parse(json['dateOfBirth'])
: null;
profileImagePath = json['profileImagePath'];
profileImageUrl = json['profileImageUrl'];
if (json['address'] != null) {
address =
PatientAddress.fromJson(json['address'] as Map<String, dynamic>);
}
if (json['familyMembers'] != null) {
familyMembers = (json['familyMembers'] as List)
.map((memberJson) => FamilyMember.fromJson(memberJson))
.toList();
}
}
void updateFrom(PatientModel other) {
name = other.name;
phoneNumber = other.phoneNumber;
gender = other.gender;
dateOfBirth = other.dateOfBirth;
profileImagePath = other.profileImagePath;
profileImageUrl = other.profileImageUrl;
address = other.address;
familyMembers = other.familyMembers;
}
}
class FamilyMember {
String? name;
String? relation;
String? gender;
DateTime? dateOfBirth;
FamilyMember({this.name, this.relation, this.gender, this.dateOfBirth});
Map<String, dynamic> toJson() {
return {
'name': name,
'relation': relation,
'gender': gender,
'dateOfBirth': dateOfBirth?.toIso8601String(),
};
}
FamilyMember.fromJson(Map<String, dynamic> json) {
name = json['name'];
relation = json['relation'];
gender = json['gender'];
dateOfBirth = json['dateOfBirth'] != null
? DateTime.parse(json['dateOfBirth'])
: null;
}
}
class PatientAddress {
String? houseNo;
String? line;
String? town;
String? pincode;
String? country;
String? state;
String? city;
String? addressType;
String? otherLabel;
PatientAddress({
this.houseNo,
this.line,
this.town,
this.pincode,
this.country,
this.state,
this.city,
this.addressType,
this.otherLabel,
});
Map<String, dynamic> toJson() {
return {
'houseNo': houseNo,
'line': line,
'town': town,
'pincode': pincode,
'country': country,
'state': state,
'city': city,
'addressType': addressType,
'otherLabel': otherLabel,
};
}
PatientAddress.fromJson(Map<String, dynamic> json) {
houseNo = json['houseNo'];
line = json['line'];
town = json['town'];
pincode = json['pincode'];
country = json['country'];
state = json['state'];
city = json['city'];
addressType = json['addressType'];
otherLabel = json['otherLabel'];
}
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:medora/data/models/telemed_user.dart'; import 'package:medora/data/models/telemed_user.dart';
import 'package:medora/data/services/data_service.dart'; import 'package:medora/data/services/data_service.dart';
import 'package:medora/data/services/doctor_profile_service.dart'; import 'package:medora/data/services/doctor_profile_service.dart';
import 'package:medora/data/services/patient_registration_service.dart';
import 'package:medora/route/route_names.dart'; import 'package:medora/route/route_names.dart';
class NavigationService { class NavigationService {
@ -17,23 +16,15 @@ class NavigationService {
return; return;
} }
switch (userProfile.role.toLowerCase()) { if (userProfile.role.toLowerCase() != 'doctor') {
case 'doctor': if (context.mounted) {
if (context.mounted) { Navigator.pushReplacementNamed(context, RouteNames.launch);
handleDoctorNavigation(context); return;
} }
}
break; if (context.mounted) {
case 'patient': await handleDoctorNavigation(context);
if (context.mounted) {
handlePatientNavigation(context);
}
break;
default:
if (context.mounted) {
Navigator.pushReplacementNamed(context, RouteNames.launch);
}
} }
} catch (e) { } catch (e) {
print('Error in handleUserNavigation: $e'); print('Error in handleUserNavigation: $e');
@ -57,25 +48,25 @@ class NavigationService {
} }
} }
static Future<void> handlePatientNavigation(BuildContext context) async { // static Future<void> handlePatientNavigation(BuildContext context) async {
try { // try {
final patientProfile = await PatientProfileService.getPatientProfile(); // final patientProfile = await PatientProfileService.getPatientProfile();
if (context.mounted) { // if (context.mounted) {
if (patientProfile != null) { // if (patientProfile != null) {
Navigator.pushReplacementNamed( // Navigator.pushReplacementNamed(
context, RouteNames.patientDashboardScreen); // context, RouteNames.patientDashboardScreen);
} else { // } else {
Navigator.pushReplacementNamed( // Navigator.pushReplacementNamed(
context, RouteNames.patientLandingScreen); // context, RouteNames.patientLandingScreen);
} // }
} // }
} catch (e) { // } catch (e) {
print('Error in handlePatientNavigation: $e'); // print('Error in handlePatientNavigation: $e');
if (context.mounted) { // if (context.mounted) {
Navigator.pushReplacementNamed( // Navigator.pushReplacementNamed(
context, RouteNames.patientLandingScreen); // context, RouteNames.patientLandingScreen);
} // }
} // }
} // }
} }

View File

@ -1,198 +0,0 @@
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:medora/controllers/patient_controller.dart';
import 'package:medora/data/models/patient.dart';
import 'package:path/path.dart' as path;
class PatientProfileService {
static final String patientProfileCollectionName =
dotenv.env['PATIENT_PROFILE_COLLECTION_NAME']!;
static final FirebaseFirestore db = FirebaseFirestore.instance;
static final FirebaseStorage storage = FirebaseStorage.instanceFor(
bucket: dotenv.env['FIREBASE_STORAGE_BUCKET']!);
static Future<String?> uploadProfileImage(File imageFile) async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return null;
}
final String uid = user.uid;
final String fileName =
'profile_${uid}_${DateTime.now().millisecondsSinceEpoch}${path.extension(imageFile.path)}';
final Reference storageRef =
storage.ref().child('profile_images/$fileName');
final UploadTask uploadTask = storageRef.putFile(
imageFile,
SettableMetadata(
contentType: 'image/${path.extension(imageFile.path).substring(1)}',
customMetadata: {
'userId': uid,
'uploadedAt': DateTime.now().toIso8601String(),
},
),
);
final TaskSnapshot snapshot = await uploadTask;
final String downloadUrl = await snapshot.ref.getDownloadURL();
print('Profile image uploaded successfully');
return downloadUrl;
} catch (e) {
print('Error uploading profile image: $e');
return null;
}
}
static Future<bool> deleteProfileImage(String imageUrl) async {
try {
final Reference storageRef = storage.refFromURL(imageUrl);
await storageRef.delete();
print('Profile image deleted successfully');
return true;
} catch (e) {
print('Error deleting profile image: $e');
return false;
}
}
static Future<bool> savePatientProfile(PatientController controller) async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return false;
}
final String uid = user.uid;
final PatientModel patientData = controller.model;
String? imageUrl;
if (patientData.profileImagePath != null) {
final File imageFile = File(patientData.profileImagePath!);
imageUrl = await uploadProfileImage(imageFile);
if (imageUrl == null) {
print('Failed to upload profile image');
return false;
}
}
final Map<String, dynamic> patientJson = patientData.toJson();
patientJson['createdAt'] = FieldValue.serverTimestamp();
patientJson['updatedAt'] = FieldValue.serverTimestamp();
patientJson['uid'] = uid;
patientJson['profileImageUrl'] = imageUrl;
await db
.collection(patientProfileCollectionName)
.doc(uid)
.set(patientJson);
print('Patient profile saved successfully');
return true;
} catch (e) {
print('Error saving patient profile: $e');
return false;
}
}
static Future<bool> updatePatientProfile(PatientModel patient) async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return false;
}
final String uid = user.uid;
String? imageUrl;
if (patient.profileImagePath != null) {
final DocumentSnapshot oldDoc =
await db.collection(patientProfileCollectionName).doc(uid).get();
if (oldDoc.exists) {
final oldData = oldDoc.data() as Map<String, dynamic>;
final String? oldImageUrl = oldData['profileImageUrl'];
if (oldImageUrl != null) {
await deleteProfileImage(oldImageUrl);
}
}
final File imageFile = File(patient.profileImagePath!);
imageUrl = await uploadProfileImage(imageFile);
if (imageUrl == null) {
print('Failed to upload new profile image');
return false;
}
}
final Map<String, dynamic> patientJson = patient.toJson();
patientJson['updatedAt'] = FieldValue.serverTimestamp();
if (imageUrl != null) {
patientJson['profileImageUrl'] = imageUrl;
}
await db
.collection(patientProfileCollectionName)
.doc(uid)
.update(patientJson);
print('Patient profile updated successfully');
return true;
} catch (e) {
print('Error updating patient profile: $e');
return false;
}
}
static Future<bool> deletePatientProfile() async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return false;
}
final String uid = user.uid;
final DocumentSnapshot doc =
await db.collection(patientProfileCollectionName).doc(uid).get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
final String? imageUrl = data['profileImageUrl'];
if (imageUrl != null) {
await deleteProfileImage(imageUrl);
}
}
await db.collection(patientProfileCollectionName).doc(uid).delete();
print('Patient profile deleted successfully');
return true;
} catch (e) {
print('Error deleting patient profile: $e');
return false;
}
}
static Future<PatientModel?> getPatientProfile() async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return null;
}
final String uid = user.uid;
final DocumentSnapshot doc =
await db.collection(patientProfileCollectionName).doc(uid).get();
if (!doc.exists) {
print('No patient profile found for this user');
return null;
}
final data = doc.data() as Map<String, dynamic>;
return PatientModel.fromJson(data);
} catch (e) {
print('Error fetching patient profile: $e');
return null;
}
}
}

View File

@ -1,7 +1,7 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:telemednet/telemed_user.dart'; import 'package:medora/data/models/telemed_user.dart';
class DataService { class DataService {
static final String profileCollectionName = static final String profileCollectionName =

View File

@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
import 'package:medora/controllers/consultation_center_controller.dart'; import 'package:medora/controllers/consultation_center_controller.dart';
import 'package:medora/data/models/consultation_center.dart'; import 'package:medora/data/models/consultation_center.dart';
import 'package:medora/data/models/doctor.dart'; import 'package:medora/data/models/doctor.dart';
import 'package:medora/data/services/navigation_service.dart';
import 'package:medora/screens/authentication/launch_screen.dart'; import 'package:medora/screens/authentication/launch_screen.dart';
// import 'package:medora/data/telemed_user.dart';
import 'package:medora/controllers/patient_controller.dart';
import 'package:medora/route/route_names.dart'; import 'package:medora/route/route_names.dart';
import 'package:medora/screens/authentication/sign_up_screen.dart';
import 'package:medora/screens/common/loading_screen.dart'; import 'package:medora/screens/common/loading_screen.dart';
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart';
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart';
@ -26,41 +26,45 @@ import 'package:medora/screens/doctor_screen/doctor_profile_screens/experience_s
import 'package:medora/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart';
import 'package:medora/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart';
import 'package:medora/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/consultation_booking_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/consultation_time_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/consultations_center_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/doctor_details_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/doctors_list_screen.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart';
import 'package:medora/screens/patient_screens/patient_dashboard/patient_dashboard_screen.dart';
import 'package:medora/screens/patient_screens/patient_dashboard/patient_home_screen.dart';
import 'package:medora/screens/patient_screens/patient_dashboard/patient_profile_screen.dart';
import 'package:medora/screens/patient_screens/registration_screens/patient_adress_screen.dart';
import 'package:medora/screens/patient_screens/registration_screens/patient_family_members_screen.dart';
import 'package:medora/screens/patient_screens/registration_screens/patient_registration_screen.dart';
import 'package:medora/screens/splash_screen.dart'; import 'package:medora/screens/splash_screen.dart';
import '../controllers/doctor_controller.dart'; import '../controllers/doctor_controller.dart';
import '../screens/patient_screens/patient_landing_screen.dart';
import '../screens/patient_screens/registration_screens/family_members_edit_screen.dart';
final Map<String, Widget Function(BuildContext)> routes = { final Map<String, Widget Function(BuildContext)> routes = {
RouteNames.launch: (context) => const LaunchScreen(), // RouteNames.launch: (context) => const LaunchScreen(),
RouteNames.signIn: (context) => SignInScreen( RouteNames.signIn: (context) => SignInScreen(
providers: [EmailAuthProvider(), PhoneAuthProvider()], providers: [EmailAuthProvider(), PhoneAuthProvider()],
showAuthActionSwitch: false,
footerBuilder: (context, action) {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, RouteNames.signUp);
},
child: const Text(
"Don't have an account? Sign up",
),
),
);
},
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
print("Sign in successful");
NavigationService.handleUserNavigation(context);
}),
AuthStateChangeAction<AuthFailed>((context, state) {
print("Sign in failed: ${state.exception}");
}),
],
), ),
RouteNames.signUp: (context) => const RegisterScreen(), RouteNames.signUp: (context) => const SignUpScreen(),
// RouteNames.userProfile: (context) { // RouteNames.userProfile: (context) {
// var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?; // var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?;
// return UserProfileScreen(user: user); // return UserProfileScreen(user: user);
// }, // },
// RouteNames.userHome: (context) => const UserScreen(), // RouteNames.userHome: (context) => const UserScreen(),
RouteNames.profileUpload: (context) => const ProfileUploadPage(),
RouteNames.patientLandingScreen: (context) => const PatientLandingScreen(),
RouteNames.patientHomeScreen: (context) => const PatientHomeScreen(),
RouteNames.doctorLandingScreen: (context) => const DoctorLandingScreen(), RouteNames.doctorLandingScreen: (context) => const DoctorLandingScreen(),
RouteNames.patientRegistrationScreen: (context) =>
const PatientRegistrationScreen(),
RouteNames.qualificationsScreen: (context) { RouteNames.qualificationsScreen: (context) {
final controller = final controller =
ModalRoute.of(context)!.settings.arguments as DoctorController?; ModalRoute.of(context)!.settings.arguments as DoctorController?;
@ -68,6 +72,7 @@ final Map<String, Widget Function(BuildContext)> routes = {
controller: controller ?? DoctorController(), // Provide fallback controller: controller ?? DoctorController(), // Provide fallback
); );
}, },
RouteNames.profileUpload: (context) => const ProfileUploadPage(),
RouteNames.doctorAddressScreen: (context) { RouteNames.doctorAddressScreen: (context) {
final controller = final controller =
ModalRoute.of(context)!.settings.arguments as DoctorController?; ModalRoute.of(context)!.settings.arguments as DoctorController?;
@ -110,65 +115,6 @@ final Map<String, Widget Function(BuildContext)> routes = {
controller: controller ?? DoctorController(), controller: controller ?? DoctorController(),
); );
}, },
RouteNames.patientAdressScreen: (context) {
final controller =
ModalRoute.of(context)!.settings.arguments as PatientController;
return PatientAddressScreen(controller: controller);
},
RouteNames.patientFamilyMembersScreen: (context) {
final controller =
ModalRoute.of(context)!.settings.arguments as PatientController;
return PatientFamilyMembersScreen(controller: controller);
},
RouteNames.familyMembersEditScreen: (context) {
final controller =
ModalRoute.of(context)!.settings.arguments as PatientController;
return FamilyMembersEditScreen(controller: controller);
},
RouteNames.patientprofileScreen: (context) => const PatientProfileScreen(),
RouteNames.patientDashboardScreen: (context) =>
const PatientDashboardScreen(),
RouteNames.specialityScreen: (context) => const SpecialtyScreen(),
RouteNames.doctorListScreen: (context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return DoctorsListScreen(
specialty: args['specialty'] as String,
);
},
RouteNames.doctorDetailsScreen: (context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return DoctorDetailsScreen(
doctor: args['doctor'] as Doctor,
preloadedImage: args['imageProvider'] as ImageProvider?,
);
},
RouteNames.consultationCenterScreen: (context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return ConsultationsCenterScreen(
doctor: args['doctor'] as Doctor,
);
},
RouteNames.consultationTimeScreen: (context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return ConsultationTimeScreen(
doctor: args['doctor'] as Doctor,
selectedConsultation: args['selectedConsultation'] as ConsultationCenter,
);
},
RouteNames.consultationBookingScreen: (context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return ConsultationBookingScreen(
doctor: args['doctor'] as Doctor,
selectedConsultation: args['selectedConsultation'] as ConsultationCenter,
selectedDate: args['selectedDate'] as DateTime,
selectedTime: args['selectedTime'] as String,
);
},
RouteNames.doctorDashbordScreen: (context) => const DoctorDashboardScreen(), RouteNames.doctorDashbordScreen: (context) => const DoctorDashboardScreen(),
RouteNames.doctorHomeScreen: (context) => const DoctorDashboardHomeScreen(), RouteNames.doctorHomeScreen: (context) => const DoctorDashboardHomeScreen(),
RouteNames.doctorPersonalProfileScreen: (context) => RouteNames.doctorPersonalProfileScreen: (context) =>

View File

@ -185,9 +185,7 @@ class _LaunchScreenState extends State<LaunchScreen> {
void _navigateToSignUp() { void _navigateToSignUp() {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SignUpScreen( builder: (context) => const SignUpScreen(),
selectedUserType: selectedUserType!,
),
), ),
); );
} }

View File

@ -7,11 +7,11 @@ import 'package:medora/data/services/navigation_service.dart';
import 'package:medora/widgets/primary_button.dart'; import 'package:medora/widgets/primary_button.dart';
class SignUpScreen extends StatefulWidget { class SignUpScreen extends StatefulWidget {
final String selectedUserType; // final String selectedUserType;
const SignUpScreen({ const SignUpScreen({
super.key, super.key,
required this.selectedUserType, // required this.selectedUserType,
}); });
@override @override
@ -160,7 +160,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
'Register as ${widget.selectedUserType}', 'Register as doctor',
style: Theme.of(context).textTheme.headlineLarge?.copyWith( style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -299,17 +299,13 @@ class _SignUpScreenState extends State<SignUpScreen> {
final result = await DataService.createUserProfile( final result = await DataService.createUserProfile(
email: _emailController.text.trim(), email: _emailController.text.trim(),
password: _passwordController.text, password: _passwordController.text,
userType: widget.selectedUserType, userType: 'doctor',
phoneNumber: _completePhoneNumber, phoneNumber: _completePhoneNumber,
); );
if (mounted) { if (mounted) {
if (result['success']) { if (result['success']) {
if (widget.selectedUserType.toLowerCase() == 'doctor') { await NavigationService.handleDoctorNavigation(context);
await NavigationService.handleDoctorNavigation(context);
} else {
await NavigationService.handlePatientNavigation(context);
}
} else { } else {
_showErrorSnackBar(result['message']); _showErrorSnackBar(result['message']);
} }

View File

@ -187,7 +187,7 @@ class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
try { try {
await _auth.signOut(); await _auth.signOut();
if (mounted) { if (mounted) {
Navigator.of(context).pushReplacementNamed(RouteNames.launch); Navigator.of(context).pushReplacementNamed(RouteNames.signIn);
} }
} catch (e) { } catch (e) {
print("Error signing out: $e"); print("Error signing out: $e");

View File

@ -1,842 +0,0 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:medora/data/models/consultation_center.dart';
import 'package:medora/data/models/doctor.dart';
import 'package:medora/data/models/patient.dart';
import 'package:medora/data/services/consultation_booking_service.dart';
import 'package:medora/data/services/patient_registration_service.dart';
import 'package:medora/route/route_names.dart';
import 'package:medora/widgets/alert_screen.dart';
class ConsultationBookingScreen extends StatefulWidget {
final Doctor doctor;
final ConsultationCenter selectedConsultation;
final DateTime selectedDate;
final String selectedTime;
const ConsultationBookingScreen({
super.key,
required this.doctor,
required this.selectedConsultation,
required this.selectedDate,
required this.selectedTime,
});
@override
State<ConsultationBookingScreen> createState() =>
_ConsultationBookingScreenState();
}
class _ConsultationBookingScreenState extends State<ConsultationBookingScreen> {
PatientModel? selectedPatient;
List<PatientModel> familyMembers = [];
FamilyMember? selectedFamilyMember;
bool isLoading = true;
final TextEditingController _nameController = TextEditingController();
final TextEditingController _relationController = TextEditingController();
DateTime? _selectedDateOfBirth;
String _selectedGender = 'Male';
@override
void dispose() {
_nameController.dispose();
_relationController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_loadPatientProfile();
}
Future<void> _loadPatientProfile() async {
setState(() => isLoading = true);
try {
final currentPatient = await PatientProfileService.getPatientProfile();
if (currentPatient != null) {
setState(() {
selectedPatient = currentPatient;
});
}
} catch (e) {
print('Error loading patient data: $e');
} finally {
setState(() => isLoading = false);
}
}
String get formattedAddress {
final parts = [
widget.selectedConsultation.floorBuilding,
widget.selectedConsultation.street,
widget.selectedConsultation.city,
widget.selectedConsultation.state,
widget.selectedConsultation.postalCode
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAppointmentCard(),
const SizedBox(height: 24),
_buildDoctorDetails(),
const SizedBox(height: 24),
_buildLocationDetails(),
const SizedBox(height: 24),
_buildPaymentDetails(),
const SizedBox(height: 24),
_buildInClinicAppointmentText(),
const SizedBox(height: 24),
_buildConfirmButton(context),
],
),
),
),
);
}
Widget _buildInClinicAppointmentText() {
String patientName =
selectedFamilyMember?.name ?? selectedPatient?.name ?? 'Select Patient';
String relation = selectedFamilyMember?.relation != null
? ' (${selectedFamilyMember!.relation})'
: '';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: RichText(
text: TextSpan(
style: GoogleFonts.poppins(
fontSize: 16,
color: Colors.black87,
),
children: [
const TextSpan(text: 'In-clinic appointment for '),
TextSpan(
text: '$patientName$relation',
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
],
),
),
),
TextButton(
onPressed: _showPatientSelectionDialog,
child: Text(
'Change',
style: GoogleFonts.poppins(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Booking Overview',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
);
}
Widget _buildAppointmentCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
children: [
const Icon(Icons.calendar_today, color: Colors.white),
const SizedBox(width: 12),
Text(
DateFormat('EEEE, MMMM d').format(widget.selectedDate),
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.access_time, color: Colors.white),
const SizedBox(width: 12),
Text(
widget.selectedTime,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildDoctorDetails() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.doctor.profileImageUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.grey[300],
child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.doctor.firstName ?? '',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
widget.doctor.speciality!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
Text(
'${widget.doctor.yearsOfExperience} years experience',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
],
),
);
}
Widget _buildLocationDetails() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Location',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
formattedAddress,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes} minutes',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
);
}
Widget _buildPaymentDetails() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Payment Details',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Consultation Fee',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
Text(
'${widget.selectedConsultation.consultationFee ?? "500"}',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget _buildConfirmButton(BuildContext context) {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Handle payment and booking confirmation
_showConfirmationDialog(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Confirm & Pay',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
);
}
void _showConfirmationDialog(BuildContext context) async {
if (selectedPatient == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a patient for the appointment'),
backgroundColor: Colors.red,
),
);
return;
}
final bookingService = BookingService();
final currentUser = FirebaseAuth.instance.currentUser;
// Get the correct patient name based on selection
final patientName = selectedFamilyMember != null
? selectedFamilyMember!.name
: selectedPatient!.name;
try {
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
}
final bookingId = await bookingService.createBooking(
doctorId: widget.doctor.uid!,
profileImageUrl: widget.doctor.profileImageUrl!,
doctorName: widget.doctor.firstName ?? 'Doctor',
patientId: currentUser!.uid,
patientName: patientName ?? 'Patient',
location: formattedAddress,
appointmentDate: widget.selectedDate,
appointmentTime: widget.selectedTime,
consultationFee:
int.parse(widget.selectedConsultation.consultationFee ?? "500"),
specialization: widget.doctor.speciality!,
);
if (context.mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AlertScreen(
arguments: AlertArguments(
title: 'Booking Confirmed',
message:
'Your in-clinic appointment has been successfully booked for $patientName. Booking ID: ${bookingId.substring(0, 8)}\n\nPlease complete the payment to confirm your appointment.',
actionTitle: 'View Appointments',
type: AlertType.success,
onActionPressed: () {
Navigator.pushReplacementNamed(
context, RouteNames.patientDashboardScreen);
},
),
),
),
);
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AlertScreen(
arguments: AlertArguments(
title: 'Booking Failed',
message: 'Unable to create booking. ${e.toString()}',
actionTitle: 'Try Again',
type: AlertType.error,
onActionPressed: () {
Navigator.of(context).pop();
},
),
),
),
);
}
}
}
Future<void> _showAddFamilyMemberDialog() async {
_nameController.clear();
_relationController.clear();
setState(() {
_selectedDateOfBirth = null;
_selectedGender = 'Male';
});
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) => StatefulBuilder(
builder: (BuildContext context, StateSetter setDialogState) {
return AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(
'Add Family Member',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Full Name',
labelStyle: GoogleFonts.poppins(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(
Icons.person_outline,
color: Colors.blue,
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _relationController,
decoration: InputDecoration(
labelText: 'Relation',
labelStyle: GoogleFonts.poppins(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(
Icons.family_restroom,
color: Colors.blue,
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.blue,
onPrimary: Colors.white,
surface: Colors.grey[100]!,
),
),
child: child!,
);
},
);
if (picked != null) {
setDialogState(() {
_selectedDateOfBirth = picked;
});
}
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.calendar_today,
color: Colors.blue),
const SizedBox(width: 12),
Text(
_selectedDateOfBirth != null
? DateFormat('dd/MM/yyyy')
.format(_selectedDateOfBirth!)
: 'Select Date of Birth',
style: GoogleFonts.poppins(),
),
],
),
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButtonFormField<String>(
value: _selectedGender,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.person_outline,
color: Colors.blue),
border: InputBorder.none,
labelStyle: GoogleFonts.poppins(),
),
items: ['Male', 'Female', 'Other']
.map((gender) => DropdownMenuItem(
value: gender,
child: Text(gender,
style: GoogleFonts.poppins()),
))
.toList(),
onChanged: (value) {
if (value != null) {
setDialogState(() => _selectedGender = value);
}
},
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: Colors.grey),
),
),
ElevatedButton(
onPressed: () => _addFamilyMember(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Add Member',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
);
},
),
);
}
Future<void> _addFamilyMember(BuildContext context) async {
if (_nameController.text.isEmpty ||
_relationController.text.isEmpty ||
_selectedDateOfBirth == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Please fill in all fields',
style: GoogleFonts.poppins(),
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
return;
}
try {
final newFamilyMember = FamilyMember(
name: _nameController.text,
relation: _relationController.text,
gender: _selectedGender,
dateOfBirth: _selectedDateOfBirth,
);
if (selectedPatient != null) {
selectedPatient!.familyMembers.add(newFamilyMember);
await PatientProfileService.updatePatientProfile(selectedPatient!);
setState(() {
selectedFamilyMember = newFamilyMember;
});
}
if (context.mounted) {
Navigator.pop(context);
_showPatientSelectionDialog();
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context); // Pop add family member dialog
}
}
}
void _showPatientSelectionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(
'Select Patient',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
// Main patient
_buildPatientTile(
name: selectedPatient?.name ?? '',
subtitle: 'Primary Patient',
isSelected: selectedFamilyMember == null,
onTap: () {
setState(() {
selectedFamilyMember = null;
});
Navigator.pop(context);
},
),
const Divider(),
// Family members
...selectedPatient?.familyMembers.map(
(member) => _buildPatientTile(
name: member.name ?? '',
subtitle: member.relation ?? '',
isSelected: selectedFamilyMember == member,
onTap: () {
setState(() {
selectedFamilyMember = member;
});
Navigator.pop(context);
},
),
) ??
[],
const Divider(),
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.person_add, color: Colors.blue),
),
title: Text(
'Add Family Member',
style: GoogleFonts.poppins(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
onTap: () {
Navigator.pop(context);
_showAddFamilyMemberDialog();
},
),
],
),
),
),
);
}
Widget _buildPatientTile({
required String name,
required String subtitle,
required bool isSelected,
required VoidCallback onTap,
}) {
return ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.withOpacity(0.1)
: Colors.grey.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.person,
color: isSelected ? Colors.blue : Colors.grey,
),
),
title: Text(
name,
style: GoogleFonts.poppins(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.black87,
),
),
subtitle: Text(
subtitle,
style: GoogleFonts.poppins(
color: Colors.grey[600],
),
),
trailing: isSelected
? const Icon(Icons.check_circle, color: Colors.blue)
: null,
onTap: onTap,
);
}
}

View File

@ -1,553 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:medora/data/models/consultation_center.dart';
import 'package:medora/data/models/doctor.dart';
import 'package:intl/intl.dart';
import 'package:medora/route/route_names.dart';
class ConsultationTimeScreen extends StatefulWidget {
final Doctor doctor;
final ConsultationCenter selectedConsultation;
const ConsultationTimeScreen({
super.key,
required this.doctor,
required this.selectedConsultation,
});
@override
State<ConsultationTimeScreen> createState() => _ConsultationTimeScreenState();
}
class _ConsultationTimeScreenState extends State<ConsultationTimeScreen> {
DateTime? selectedDate;
String? selectedTime;
List<TimeSlot> getTimeSlotsForDay(String dayName) {
try {
final schedule = widget.selectedConsultation.weeklySchedule?.firstWhere(
(schedule) => schedule.day == dayName,
orElse: () => AvailabilitySchedule(
day: dayName,
timeSlots: [],
),
);
return schedule?.timeSlots ?? [];
} catch (e) {
debugPrint('Error getting time slots: $e');
return [];
}
}
DateTime? parseTimeString(String? timeStr) {
if (timeStr == null) return null;
try {
// Try parsing 12-hour format first
return DateFormat('h:mm a').parse(timeStr);
} catch (e) {
try {
// Try parsing 24-hour format
return DateFormat('HH:mm').parse(timeStr);
} catch (e) {
debugPrint('Error parsing time: $timeStr');
return null;
}
}
}
String get formattedAddress {
final parts = [
widget.selectedConsultation.floorBuilding,
widget.selectedConsultation.street,
widget.selectedConsultation.city,
widget.selectedConsultation.state,
widget.selectedConsultation.postalCode
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
appBar: _buildAppBar(),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLocationInfo(),
const SizedBox(height: 24),
_buildDateSelection(),
const SizedBox(height: 24),
_buildTimeSlots(),
],
),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Select Date & Time',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
);
}
Widget _buildLocationInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.doctor.firstName ?? '',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
widget.doctor.speciality!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.doctor.profileImageUrl!,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 60,
height: 60,
color: Colors.grey[300],
child:
Icon(Icons.person, size: 30, color: Colors.grey[600]),
);
},
),
),
],
),
const Divider(height: 24),
Text(
'Selected Location',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
widget.selectedConsultation.city ?? '',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
Text(
'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes}',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
);
}
Widget _buildDateSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Date',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 20, // Show next 20 days
itemBuilder: (context, index) {
final date = DateTime.now().add(Duration(days: index));
final isSelected = selectedDate?.day == date.day &&
selectedDate?.month == date.month &&
selectedDate?.year == date.year;
final isAvailable = _isDateAvailable(date);
return GestureDetector(
onTap: isAvailable
? () {
setState(() {
selectedDate = date;
selectedTime = null; // Reset time when date changes
});
}
: null,
child: Container(
width: 70,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? Colors.blue
: isAvailable
? Colors.white
: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
boxShadow: isAvailable
? [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.white
: isAvailable
? Colors.grey[600]
: Colors.grey[400],
),
),
const SizedBox(height: 8),
Text(
date.day.toString(),
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: isAvailable
? Colors.black87
: Colors.grey[400],
),
),
],
),
),
);
},
),
),
],
);
}
Widget _buildTimeSlots() {
if (selectedDate == null) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: Center(
child: Text(
'Please select a date to view available time slots',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
),
);
}
final dayName = DateFormat('EEEE').format(selectedDate!);
final timeSlots = getTimeSlotsForDay(dayName);
final allTimeSlots = _generateTimeSlots(timeSlots);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Time',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Wrap(
spacing: 12,
runSpacing: 12,
children: allTimeSlots.map((time) {
final isSelected = selectedTime == time;
final isAvailable = _isTimeSlotAvailable(time);
return GestureDetector(
onTap: isAvailable
? () {
Navigator.pushNamed(
context,
RouteNames.consultationBookingScreen,
arguments: {
'doctor': widget.doctor,
'selectedConsultation': widget.selectedConsultation,
'selectedDate': selectedDate,
'selectedTime': time
},
);
}
: null,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? Colors.blue
: isAvailable
? Colors.white
: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? Colors.blue
: isAvailable
? Colors.grey.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
),
),
child: Text(
time,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.white
: isAvailable
? Colors.black87
: Colors.grey[400],
),
),
),
);
}).toList(),
),
),
],
);
}
bool _isDateAvailable(DateTime date) {
final dayName = DateFormat('EEEE').format(date);
return widget.selectedConsultation.weeklySchedule
?.any((schedule) => schedule.day == dayName) ??
false;
}
List<String> _generateTimeSlots(List<TimeSlot> timeSlots) {
final slots = <String>[];
final timeFormat = DateFormat('h:mm a');
for (var slot in timeSlots) {
final startTime = parseTimeString(slot.startTime);
final endTime = parseTimeString(slot.endTime);
if (startTime == null || endTime == null) continue;
var currentTime = startTime;
while (currentTime.isBefore(endTime)) {
slots.add(timeFormat.format(currentTime));
currentTime = currentTime.add(const Duration(minutes: 30));
}
}
return slots;
}
bool _isTimeSlotAvailable(String time) {
final now = DateTime.now();
if (selectedDate == null) return false;
// Parse the time slot
final timeSlot = parseTimeString(time);
if (timeSlot == null) return false;
// Create a DateTime combining selected date and time
final slotDateTime = DateTime(
selectedDate!.year,
selectedDate!.month,
selectedDate!.day,
timeSlot.hour,
timeSlot.minute,
);
// Check if the slot is in the past
if (slotDateTime.isBefore(now)) return false;
// Here you would typically check against your booking database
// For now, returning true for future slots
return true;
}
void _handleBooking() {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
'Confirm Booking',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildConfirmationDetail(
'Doctor',
widget.doctor.firstName!,
),
const SizedBox(height: 8),
_buildConfirmationDetail(
'Location',
widget.selectedConsultation.city!,
),
const SizedBox(height: 8),
_buildConfirmationDetail(
'Date',
DateFormat('EEEE, MMMM d').format(selectedDate!),
),
const SizedBox(height: 8),
_buildConfirmationDetail(
'Time',
selectedTime!,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: GoogleFonts.poppins(
color: Colors.grey[600],
),
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Confirm',
style: GoogleFonts.poppins(
color: Colors.white,
),
),
),
],
),
);
}
Widget _buildConfirmationDetail(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
'$label:',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
),
Expanded(
child: Text(
value,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}

View File

@ -1,312 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:medora/data/models/doctor.dart';
import 'package:medora/data/models/consultation_center.dart';
import 'package:medora/data/services/consultation_center_service.dart';
import 'package:medora/route/route_names.dart';
class ConsultationsCenterScreen extends StatefulWidget {
final Doctor doctor;
const ConsultationsCenterScreen({
super.key,
required this.doctor,
});
@override
State<ConsultationsCenterScreen> createState() =>
_ConsultationsCenterScreenState();
}
class _ConsultationsCenterScreenState extends State<ConsultationsCenterScreen> {
List<ConsultationCenter> _consultationCenters = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_fetchDoctorConsultationCenters();
}
Future<void> _fetchDoctorConsultationCenters() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
if (widget.doctor.uid == null) {
throw Exception('Doctor UID is missing');
}
final centers =
await ConsultationCenterService.getDoctorConsultationCenters(
widget.doctor.uid!,
);
if (mounted) {
setState(() {
_consultationCenters = centers;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load consultation centers: $e')),
);
}
}
}
String _formatAddress(ConsultationCenter center) {
List<String> addressParts = [];
if (center.floorBuilding != null && center.floorBuilding!.isNotEmpty) {
addressParts.add(center.floorBuilding!);
}
if (center.street != null && center.street!.isNotEmpty) {
addressParts.add(center.street!);
}
if (center.city != null && center.city!.isNotEmpty) {
addressParts.add(center.city!);
}
return addressParts.join(', ');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
appBar: _buildAppBar(),
body: RefreshIndicator(
onRefresh: _fetchDoctorConsultationCenters,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDoctorInfo(),
const SizedBox(height: 24),
_buildConsultationLocations(),
],
),
),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Select Location',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
);
}
Widget _buildDoctorInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.doctor.profileImageUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.grey[300],
child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.doctor.firstName ?? "",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
widget.doctor.speciality!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
Text(
'${widget.doctor.yearsOfExperience} years experience',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
],
),
);
}
Widget _buildConsultationLocations() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Location',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_error != null)
Center(
child: Column(
children: [
Text(
'Error loading centers',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.red,
),
),
TextButton(
onPressed: _fetchDoctorConsultationCenters,
child: const Text('Retry'),
),
],
),
)
else if (_consultationCenters.isEmpty)
Center(
child: Text(
'No consultation centers available',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
)
else
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _consultationCenters.length,
itemBuilder: (context, index) {
final center = _consultationCenters[index];
return GestureDetector(
onTap: () {
Navigator.pushNamed(
context,
RouteNames.consultationTimeScreen,
arguments: {
'doctor': widget.doctor,
'selectedConsultation': center,
},
);
},
child: Container(
width: 200,
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatAddress(center),
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
if (center.averageDurationMinutes != null)
Text(
'Average time: ${center.averageDurationMinutes} mins',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey[600],
),
),
if (center.consultationFee != null)
Text(
'Fee: ${center.consultationFee}',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
);
},
),
),
],
);
}
}

View File

@ -1,346 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:medora/data/models/doctor.dart';
import 'package:medora/route/route_names.dart';
import 'package:shimmer/shimmer.dart';
class DoctorDetailsScreen extends StatefulWidget {
final Doctor doctor;
final ImageProvider? preloadedImage;
const DoctorDetailsScreen({
super.key,
required this.doctor,
this.preloadedImage,
});
@override
State<DoctorDetailsScreen> createState() => _DoctorDetailsScreenState();
}
class _DoctorDetailsScreenState extends State<DoctorDetailsScreen> {
bool isDescriptionExpanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
body: Column(
children: [
_buildAppBar(context),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDoctorCard(),
const SizedBox(height: 24),
_buildDescription(),
const SizedBox(height: 24),
_buildQualifications(),
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context, RouteNames.consultationCenterScreen,
arguments: {
'doctor': widget.doctor,
});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey[300],
),
child: Text(
'Confirm Booking',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
);
}
Widget _buildAppBar(BuildContext context) {
return Container(
color: Colors.white,
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Doctor',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
),
);
}
Widget _buildDoctorCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDoctorImage(),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.doctor.firstName ?? '',
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
Text(
widget.doctor.speciality!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.medical_services,
size: 16, color: Colors.blue[400]),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.doctor.speciality!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
),
],
),
Row(
children: [
Icon(Icons.location_on,
size: 16, color: Colors.blue[400]),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.doctor.city!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
),
],
),
Row(
children: [
Icon(Icons.star, size: 16, color: Colors.blue[400]),
const SizedBox(width: 4),
Text(
'${widget.doctor.yearsOfExperience} Years',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
],
),
],
),
);
}
Widget _buildDoctorImage() {
final imageProvider =
widget.preloadedImage ?? NetworkImage(widget.doctor.profileImageUrl!);
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image(
image: imageProvider,
width: 100,
height: 100,
fit: BoxFit.cover,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
return child;
}
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.person,
size: 50,
color: Colors.grey[600],
),
);
},
),
);
}
Widget _buildDescription() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
widget.doctor.profileDescription!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
maxLines: isDescriptionExpanded ? null : 3,
overflow: isDescriptionExpanded ? null : TextOverflow.ellipsis,
),
Align(
alignment: Alignment.topLeft,
child: TextButton(
onPressed: () {
setState(() {
isDescriptionExpanded = !isDescriptionExpanded;
});
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
isDescriptionExpanded ? 'Show less' : 'Read more',
style: GoogleFonts.poppins(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
Widget _buildQualifications() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Qualifications',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
widget.doctor.qualifications?.join(', ') ?? '',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
);
}
}

View File

@ -1,387 +0,0 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:medora/data/models/doctor.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:medora/route/route_names.dart';
import 'package:shimmer/shimmer.dart';
class DoctorsListScreen extends StatefulWidget {
final String specialty;
const DoctorsListScreen({
super.key,
required this.specialty,
});
@override
State<DoctorsListScreen> createState() => _DoctorsListScreenState();
}
class _DoctorsListScreenState extends State<DoctorsListScreen> {
late final Query doctorsQuery;
final TextEditingController _searchController = TextEditingController();
bool _isSearching = false;
@override
void initState() {
super.initState();
doctorsQuery = FirebaseFirestore.instance
.collection('doctorprofiles')
.where('speciality', isEqualTo: widget.specialty);
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
setState(() {
_isSearching = _searchController.text.isNotEmpty;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
SliverToBoxAdapter(
child: _buildSearchBar(),
),
_buildDoctorsList(),
],
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 55,
floating: true,
pinned: true,
stretch: true,
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
flexibleSpace: FlexibleSpaceBar(
title: Text(
'${widget.specialty} Specialists',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
),
);
}
Widget _buildSearchBar() {
return Padding(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search doctors...',
hintStyle: GoogleFonts.poppins(
color: Colors.grey,
fontSize: 14,
),
prefixIcon: const Icon(Icons.search, color: Colors.grey),
suffixIcon: _isSearching
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
FocusScope.of(context).unfocus();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
),
),
),
);
}
Widget _buildDoctorsList() {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: StreamBuilder<QuerySnapshot>(
stream: doctorsQuery.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildShimmerDoctorCard(),
),
);
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.medical_services_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'No doctors available in this specialty',
style: GoogleFonts.poppins(
fontSize: 16,
color: Colors.grey[600],
),
),
],
),
),
);
}
final doctors = snapshot.data!.docs
.map((doc) => Doctor.fromJson(doc.data() as Map<String, dynamic>))
.where((doctor) {
if (_searchController.text.isEmpty) return true;
final searchQuery = _searchController.text.toLowerCase();
return doctor.firstName!.toLowerCase().contains(searchQuery) ||
doctor.city!.toLowerCase().contains(searchQuery);
}).toList();
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final doctor = doctors[index];
return _buildDoctorCard(doctor);
},
childCount: doctors.length,
),
);
},
),
);
}
Widget _buildShimmerDoctorCard() {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 120,
height: 20,
color: Colors.white,
),
const SizedBox(height: 8),
Container(
width: 150,
height: 16,
color: Colors.white,
),
const SizedBox(height: 8),
Container(
width: 100,
height: 16,
color: Colors.white,
),
],
),
),
],
),
),
),
);
}
Widget _buildDoctorCard(Doctor doctor) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
precacheImage(NetworkImage(doctor.profileImageUrl!), context);
Navigator.pushNamed(
context,
RouteNames.doctorDetailsScreen,
arguments: {
'doctor': doctor,
'imageProvider': NetworkImage(doctor.profileImageUrl!),
},
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildDoctorImage(doctor),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
doctor.firstName ?? '',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'${doctor.yearsOfExperience!} years experience',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
doctor.city!,
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey[400],
),
],
),
),
),
),
),
);
}
Widget _buildDoctorImage(Doctor doctor) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
doctor.profileImageUrl!,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
color: Colors.white,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: Icon(
Icons.person,
size: 40,
color: Colors.grey[400],
),
);
},
),
),
);
}
}

View File

@ -1,426 +0,0 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:animate_do/animate_do.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:medora/route/route_names.dart';
class Specialty {
final String name;
final IconData icon;
final Color color;
final String description;
Specialty({
required this.name,
required this.icon,
required this.color,
required this.description,
});
}
class SpecialtyScreen extends StatefulWidget {
const SpecialtyScreen({super.key});
@override
State<SpecialtyScreen> createState() => _SpecialtyScreenState();
}
class _SpecialtyScreenState extends State<SpecialtyScreen> {
final List<Specialty> _allSpecialties = [
Specialty(
name: 'Pediatric',
icon: Icons.child_care,
color: Colors.blue,
description: 'Medical care for infants, children, and adolescents',
),
Specialty(
name: 'General Medicine',
icon: Icons.medical_services,
color: Colors.green,
description:
'Primary healthcare for adults and general medical conditions',
),
Specialty(
name: 'Family Medicine',
icon: Icons.family_restroom,
color: Colors.teal,
description: 'Comprehensive healthcare for families and individuals',
),
Specialty(
name: 'Cardiologist',
icon: Icons.favorite,
color: Colors.red,
description: 'Diagnosis and treatment of heart conditions',
),
Specialty(
name: 'Neurology',
icon: Icons.psychology,
color: Colors.purple,
description: 'Treatment of nervous system disorders',
),
Specialty(
name: 'Gastroenterology',
icon: Icons.local_hospital,
color: Colors.orange,
description: 'Digestive system disorders and treatment',
),
Specialty(
name: 'Dermatologist',
icon: Icons.face,
color: Colors.pink,
description: 'Skin, hair, and nail conditions',
),
Specialty(
name: 'Orthopedic',
icon: Icons.wheelchair_pickup,
color: Colors.indigo,
description: 'Musculoskeletal system and injury treatment',
),
Specialty(
name: 'Ophthalmology',
icon: Icons.remove_red_eye,
color: Colors.brown,
description: 'Eye care and vision treatment',
),
Specialty(
name: 'ENT',
icon: Icons.hearing,
color: Colors.cyan,
description: 'Ear, nose, and throat specialist',
),
Specialty(
name: 'Psychiatry',
icon: Icons.psychology_outlined,
color: Colors.deepPurple,
description: 'Mental health and behavioral disorders',
),
Specialty(
name: 'Gynecology',
icon: Icons.pregnant_woman,
color: Colors.pinkAccent,
description: "Women's health and reproductive care",
),
Specialty(
name: 'Urology',
icon: Icons.water_drop,
color: Colors.lightBlue,
description: 'Urinary tract and male reproductive health',
),
Specialty(
name: 'Endocrinology',
icon: Icons.biotech,
color: Colors.amber,
description: 'Hormone and metabolic disorders',
),
Specialty(
name: 'Oncology',
icon: Icons.bloodtype,
color: Colors.redAccent,
description: 'Cancer diagnosis and treatment',
),
Specialty(
name: 'Rheumatology',
icon: Icons.accessibility,
color: Colors.deepOrange,
description: 'Arthritis and autoimmune conditions',
),
Specialty(
name: 'Pulmonology',
icon: Icons.air,
color: Colors.lightGreen,
description: 'Respiratory system disorders',
),
Specialty(
name: 'Nephrology',
icon: Icons.water,
color: Colors.blueGrey,
description: 'Kidney diseases and disorders',
),
Specialty(
name: 'Dentistry',
icon: Icons.cleaning_services,
color: Colors.cyan,
description: 'Oral health and dental care',
),
Specialty(
name: 'Physical Therapy',
icon: Icons.accessibility_new,
color: Colors.deepPurple,
description: 'Rehabilitation and physical medicine',
),
Specialty(
name: 'Sports Medicine',
icon: Icons.sports,
color: Colors.green,
description: 'Athletic injuries and performance',
),
Specialty(
name: 'Allergy & Immunology',
icon: Icons.sick,
color: Colors.orange,
description: 'Allergies and immune system disorders',
),
Specialty(
name: 'Pain Management',
icon: Icons.healing,
color: Colors.red,
description: 'Chronic pain treatment',
),
Specialty(
name: 'Sleep Medicine',
icon: Icons.bedtime,
color: Colors.indigo,
description: 'Sleep disorders and treatment',
),
Specialty(
name: 'Geriatrics',
icon: Icons.elderly,
color: Colors.brown,
description: 'Healthcare for elderly patients',
),
];
late List<Specialty> _filteredSpecialties;
final TextEditingController _searchController = TextEditingController();
bool _isSearching = false;
@override
void initState() {
super.initState();
_filteredSpecialties = _allSpecialties;
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
final searchQuery = _searchController.text.toLowerCase();
setState(() {
_isSearching = searchQuery.isNotEmpty;
_filteredSpecialties = _allSpecialties
.where((specialty) =>
specialty.name.toLowerCase().contains(searchQuery) ||
specialty.description.toLowerCase().contains(searchQuery))
.toList();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FF),
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
SliverToBoxAdapter(
child: _buildSearchBar(),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: _buildSpecialtiesGrid(),
),
],
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 55,
floating: true,
pinned: true,
stretch: true,
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.pop(context),
),
flexibleSpace: FlexibleSpaceBar(
title: Text(
'Find a Specialist',
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.w600,
fontSize: 20,
),
),
centerTitle: true,
),
);
}
Widget _buildSearchBar() {
return FadeIn(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search specialties...',
hintStyle: GoogleFonts.poppins(
color: Colors.grey,
fontSize: 14,
),
prefixIcon: const Icon(Icons.search, color: Colors.grey),
suffixIcon: _isSearching
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
FocusScope.of(context).unfocus();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
),
),
),
),
);
}
Widget _buildSpecialtiesGrid() {
return SliverAnimationBuilder(
child: MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _filteredSpecialties.length,
itemBuilder: (context, index) {
final specialty = _filteredSpecialties[index];
return FadeInUp(
delay: Duration(milliseconds: 100 * index),
child: _buildSpecialtyCard(specialty),
);
},
),
);
}
Widget _buildSpecialtyCard(Specialty specialty) {
return GestureDetector(
onTap: () {
Navigator.pushNamed(
context,
RouteNames.doctorListScreen,
arguments: {
'specialty': specialty.name,
},
);
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: specialty.color.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Positioned(
right: -20,
top: -20,
child: CircleAvatar(
radius: 40,
backgroundColor: specialty.color.withOpacity(0.1),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: specialty.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
specialty.icon,
color: specialty.color,
size: 28,
),
),
const SizedBox(height: 16),
Text(
specialty.name,
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Text(
specialty.description,
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
);
}
}
class SliverAnimationBuilder extends StatelessWidget {
final Widget child;
const SliverAnimationBuilder({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return SliverAnimatedList(
initialItemCount: 1,
itemBuilder: (context, index, animation) {
return SlideInUp(
from: 50,
child: child,
);
},
);
}
}

View File

@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:medora/screens/patient_screens/patient_dashboard/patient_home_screen.dart';
import 'package:medora/screens/patient_screens/patient_dashboard/patient_profile_screen.dart';
class PatientDashboardScreen extends StatefulWidget {
const PatientDashboardScreen({super.key});
@override
State<PatientDashboardScreen> createState() => _PatientDashboardScreenState();
}
class _PatientDashboardScreenState extends State<PatientDashboardScreen> {
int _selectedIndex = 0;
final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
// Add your pages here
final List<Widget> _pages = [
const PatientHomeScreen(),
const Center(child: Text('Chat')), // Replace with your chat screen
const Center(child: Text('Records')), // Replace with your records screen
const PatientProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: _pages[_selectedIndex],
),
bottomNavigationBar: CurvedNavigationBar(
key: _bottomNavigationKey,
backgroundColor: Colors.transparent,
color: Colors.blue,
buttonBackgroundColor: Colors.blue,
height: 60,
index: _selectedIndex,
items: const [
Icon(Icons.home, size: 30, color: Colors.white),
Icon(Icons.chat_bubble, size: 30, color: Colors.white),
Icon(Icons.assignment, size: 30, color: Colors.white),
Icon(Icons.person, size: 30, color: Colors.white),
],
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}

View File

@ -1,695 +0,0 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:medora/data/models/consultation_booking.dart';
import 'package:medora/data/services/consultation_booking_service.dart';
import 'package:medora/route/route_names.dart';
import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart';
class PatientHomeScreen extends StatefulWidget {
const PatientHomeScreen({super.key});
@override
State<PatientHomeScreen> createState() => _PatientHomeScreenState();
}
class _PatientHomeScreenState extends State<PatientHomeScreen>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
final BookingService _bookingService = BookingService();
late Stream<List<Booking>> _bookingsStream;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animationController.forward();
final User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
final String userId = user.uid;
_bookingsStream = _bookingService.getPatientBookings(userId);
} else {
_bookingsStream = const Stream.empty();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
_buildSearchBar(),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildRealTimeCard(),
const SizedBox(height: 20),
_buildConsultationsSection(),
const SizedBox(height: 20),
_buildFindDoctorSection(),
],
),
),
],
),
),
);
}
Widget _buildSearchBar() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30.0),
bottomRight: Radius.circular(30.0),
),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: TextField(
decoration: InputDecoration(
hintText: 'Search Doctor/Hospital/Symptoms',
hintStyle: GoogleFonts.poppins(
color: Colors.grey[400],
),
prefixIcon: const Icon(Icons.search, color: Colors.blue),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
),
),
),
],
),
);
}
Widget _buildRealTimeCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[400]!, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Real-time care\nat your fingertips.',
style: GoogleFonts.poppins(
fontSize: 30,
fontWeight: FontWeight.bold,
color: const Color.fromARGB(221, 67, 67, 67),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SpecialtyScreen()),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue[700],
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: Text(
'Start Consultation',
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
Widget _buildConsultationsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Upcoming Consultations',
style: GoogleFonts.poppins(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
],
),
),
const SizedBox(height: 20),
SizedBox(
height: 201,
child: StreamBuilder<List<Booking>>(
stream: _bookingsStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 12),
Text(
'Loading consultations...',
style: GoogleFonts.poppins(
color: Colors.grey[600],
),
),
],
),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline,
color: Colors.red[400], size: 48),
const SizedBox(height: 12),
Text(
'Error loading consultations',
style: GoogleFonts.poppins(
color: Colors.red[400],
fontWeight: FontWeight.w500,
),
),
TextButton(
onPressed: () {
// Implement refresh logic
},
child: Text(
'Try Again',
style: GoogleFonts.poppins(
color: Colors.blue[700],
),
),
),
],
),
);
}
final bookings = snapshot.data ?? [];
if (bookings.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.calendar_today,
color: Colors.grey[400], size: 48),
const SizedBox(height: 12),
Text(
'No upcoming consultations',
style: GoogleFonts.poppins(
color: Colors.grey[600],
fontSize: 16,
),
),
TextButton(
onPressed: () {
// Navigate to book consultation
},
child: Text(
'Book a Consultation',
style: GoogleFonts.poppins(
color: Colors.blue[700],
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20),
scrollDirection: Axis.horizontal,
itemCount: bookings.length,
itemBuilder: (context, index) {
final booking = bookings[index];
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Hero(
tag: 'consultation_${booking.id}',
child: Material(
child: _consultationCard(
booking.profileImageUrl,
booking.doctorName,
'${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}',
booking.specialization,
booking.paymentStatus,
),
),
),
);
},
);
},
),
),
],
);
}
Widget _consultationCard(
String? profileImageUrl,
String name,
String schedule,
String speciality,
PaymentStatus paymentStatus,
) {
return Container(
width: 300,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Colors.grey[50]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (profileImageUrl != null)
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(profileImageUrl),
fit: BoxFit.cover,
),
boxShadow: [
BoxShadow(
color: Colors.blue[300]!.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
)
else
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[400]!, Colors.blue[600]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.blue[300]!.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.person,
size: 36,
color: Colors.white,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
Text(
speciality,
style: GoogleFonts.poppins(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: _getStatusColor(paymentStatus).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color:
_getStatusColor(paymentStatus).withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getStatusIcon(paymentStatus),
size: 14,
color: _getStatusColor(paymentStatus),
),
const SizedBox(width: 4),
Text(
_getStatusText(paymentStatus),
style: GoogleFonts.poppins(
fontSize: 12,
color: _getStatusColor(paymentStatus),
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.blue[100]!,
),
),
child: Row(
children: [
Icon(
Icons.calendar_today,
size: 18,
color: Colors.blue[700],
),
const SizedBox(width: 8),
Text(
schedule,
style: GoogleFonts.poppins(
color: Colors.blue[700],
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
IconData _getStatusIcon(PaymentStatus status) {
switch (status) {
case PaymentStatus.completed:
return Icons.check_circle;
case PaymentStatus.pending:
return Icons.access_time;
case PaymentStatus.failed:
return Icons.error;
default:
return Icons.info;
}
}
Color _getStatusColor(PaymentStatus status) {
switch (status) {
case PaymentStatus.pending:
return Colors.orange;
case PaymentStatus.completed:
return Colors.green;
case PaymentStatus.failed:
return Colors.red;
default:
return Colors.grey;
}
}
String _getStatusText(PaymentStatus status) {
switch (status) {
case PaymentStatus.pending:
return 'Payment Pending';
case PaymentStatus.completed:
return 'Confirmed';
case PaymentStatus.failed:
return 'Payment Failed';
default:
return 'Unknown';
}
}
Widget _buildFindDoctorSection() {
final specialistData = [
{
'icon': Icons.local_hospital,
'label': 'General',
'color': Colors.blue,
'description': 'Primary Healthcare'
},
{
'icon': Icons.remove_red_eye,
'label': 'Eye',
'color': Colors.indigo,
'description': 'Vision Care'
},
{
'icon': Icons.medical_services,
'label': 'Dental',
'color': Colors.amber,
'description': 'Oral Health'
},
{
'icon': Icons.favorite,
'label': 'Cardio',
'color': Colors.red,
'description': 'Heart Specialist'
},
{
'icon': Icons.psychology,
'label': 'Mental',
'color': Colors.green,
'description': 'Mental Health'
},
{
'icon': Icons.child_care,
'label': 'Pediatric',
'color': Colors.purple,
'description': 'Child Care'
},
{
'icon': Icons.elderly,
'label': 'Geriatric',
'color': Colors.teal,
'description': 'Senior Care'
},
{
'icon': Icons.fitness_center,
'label': 'Physio',
'color': Colors.orange,
'description': 'Physical Therapy'
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'Find Specialists',
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final data in specialistData)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _specialistCard(
icon: data['icon'] as IconData,
label: data['label'] as String,
color: data['color'] as Color,
description: data['description'] as String,
),
),
const SizedBox(width: 8),
],
),
),
),
IconButton(
icon: const Icon(Icons.arrow_forward, color: Colors.blue),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SpecialtyScreen()),
);
},
),
],
),
],
);
}
Widget _specialistCard({
required IconData icon,
required String label,
required Color color,
required String description,
}) {
return GestureDetector(
onTap: () {
Navigator.pushNamed(
context,
RouteNames.doctorListScreen,
arguments: {
'specialty': label,
},
);
},
child: Container(
width: 140,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
label,
style: GoogleFonts.poppins(
color: Colors.black87,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
description,
style: GoogleFonts.poppins(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
);
}
}

View File

@ -1,197 +0,0 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:medora/data/services/patient_registration_service.dart';
import 'package:medora/route/route_names.dart';
import 'package:medora/data/models/patient.dart';
class PatientProfileScreen extends StatefulWidget {
const PatientProfileScreen({super.key});
@override
State<PatientProfileScreen> createState() => _PatientProfileScreenState();
}
class _PatientProfileScreenState extends State<PatientProfileScreen> {
final FirebaseAuth _auth = FirebaseAuth.instance;
PatientModel? _patientProfile;
@override
void initState() {
super.initState();
_fetchPatientProfile();
}
Future<void> _fetchPatientProfile() async {
final patientProfile = await PatientProfileService.getPatientProfile();
if (mounted) {
setState(() {
_patientProfile = patientProfile;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
_buildProfileHeader(),
_buildProfileOptions(),
],
),
),
);
}
Widget _buildProfileHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF00BCD4),
Color(0xFF2196F3),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
image: _patientProfile?.profileImageUrl != null
? DecorationImage(
image: NetworkImage(_patientProfile!.profileImageUrl!),
fit: BoxFit.cover,
)
: null,
),
child: _patientProfile?.profileImageUrl == null
? Center(
child: Text(
_patientProfile != null && _patientProfile!.name != null
? _patientProfile!.name![0].toUpperCase()
: '',
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
)
: null,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_patientProfile != null && _patientProfile!.name != null
? _patientProfile!.name!
: 'Create your profile',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const Icon(
Icons.chevron_right,
color: Colors.white,
size: 30,
),
],
),
);
}
Widget _buildProfileOptions() {
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 5,
),
],
),
child: Column(
children: [
_buildOptionTile(
'Medical Profile',
Icons.medical_information_outlined,
onTap: () {
// Add navigation or action
},
),
const Divider(height: 1),
_buildOptionTile(
'Sign Out',
Icons.logout,
onTap: () {
_signOut();
},
iconColor: Colors.blue,
),
],
),
);
}
Widget _buildOptionTile(String title, IconData icon,
{required VoidCallback onTap, Color? iconColor}) {
return ListTile(
leading: Icon(
icon,
color: iconColor ?? Colors.grey,
size: 24,
),
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.grey,
),
onTap: onTap,
);
}
Future<void> _signOut() async {
try {
await _auth.signOut();
if (mounted) {
Navigator.of(context).pushReplacementNamed(RouteNames.launch);
}
} catch (e) {
print("Error signing out: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to log out. Please try again.')),
);
}
}
}
}

View File

@ -1,94 +0,0 @@
import 'package:medora/route/route_names.dart';
import 'package:flutter/material.dart';
class PatientLandingScreen extends StatelessWidget {
const PatientLandingScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.teal.shade100, Colors.white],
),
),
child: SafeArea(
child: Column(
children: [
Expanded(
child: Center(
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.topRight,
child: TextButton(
onPressed: () {
Navigator.of(context).pushNamed(
RouteNames.patientDashboardScreen);
},
child: Text(
'Skip',
style: TextStyle(
color: Colors.teal.shade300,
fontSize: 16,
fontWeight: FontWeight.bold),
),
),
),
Image.asset(
'images/patient-avathar.png',
height: 200,
width: 200,
),
const SizedBox(height: 24),
const Text(
'Set your medical profile',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(
RouteNames.patientRegistrationScreen);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
minimumSize: const Size(double.infinity, 50),
),
child: const Text(
'Continue',
style:
TextStyle(fontSize: 18, color: Colors.white),
),
),
],
),
),
),
),
),
],
),
),
),
);
}
}

View File

@ -1,285 +0,0 @@
import 'package:flutter/material.dart';
import 'package:medora/controllers/patient_controller.dart';
import 'package:medora/data/models/patient.dart';
class FamilyMembersEditScreen extends StatefulWidget {
final FamilyMember? familyMember;
final PatientController controller;
const FamilyMembersEditScreen(
{super.key, this.familyMember, required this.controller});
@override
State<FamilyMembersEditScreen> createState() =>
_FamilyMembersEditScreenState();
}
class _FamilyMembersEditScreenState extends State<FamilyMembersEditScreen> {
late TextEditingController nameController;
late TextEditingController relationController;
late TextEditingController genderController;
late TextEditingController dobController;
Map<String, String> errors = {};
@override
void initState() {
super.initState();
nameController =
TextEditingController(text: widget.familyMember?.name ?? '');
relationController =
TextEditingController(text: widget.familyMember?.relation ?? '');
genderController =
TextEditingController(text: widget.familyMember?.gender ?? '');
dobController = TextEditingController(
text: widget.familyMember?.dateOfBirth?.toString().split(' ')[0] ?? '');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Family Member'),
actions: _buildAppBarActions(),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField(nameController, 'Name', Icons.person, 'name'),
_buildDropdownField(
'Relation',
relationController.text,
(String? newValue) {
setState(() {
relationController.text = newValue ?? '';
});
},
Icons.family_restroom,
),
_buildDropdownField(
'Gender',
genderController.text,
(String? newValue) {
setState(() {
genderController.text = newValue ?? '';
});
},
Icons.transgender,
),
_buildDateField(context),
],
),
),
);
}
Widget _buildDropdownField(
String label, String value, Function(String?) onChanged, IconData icon) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, color: Colors.blue),
border: const OutlineInputBorder(),
),
value: value.isEmpty ? null : value,
onChanged: onChanged,
items: label == 'Relation'
? <String>['Father', 'Mother', 'Son', 'Daughter', 'Other']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList()
: <String>['Male', 'Female', 'Other']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
);
}
Widget _buildDateField(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: dobController,
decoration: const InputDecoration(
labelText: 'Date of Birth',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today, color: Colors.blue),
),
readOnly: true,
onTap: () async {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now().subtract(const Duration(days: 365)),
firstDate: DateTime(1900),
lastDate: DateTime.now().subtract(const Duration(days: 365)),
);
if (pickedDate != null) {
setState(() {
dobController.text = pickedDate.toString().split(' ')[0];
});
}
},
),
);
}
bool _validateFields() {
errors.clear();
if (nameController.text.trim().isEmpty) {
errors['name'] = 'Name is required';
} else if (nameController.text.trim().length < 2) {
errors['name'] = 'Name must be at least 2 characters';
}
if (relationController.text.isEmpty) {
errors['relation'] = 'Please select a relation';
}
if (genderController.text.isEmpty) {
errors['gender'] = 'Please select a gender';
}
if (dobController.text.isEmpty) {
errors['dob'] = 'Date of Birth is required';
} else {
final dob = DateTime.tryParse(dobController.text);
if (dob == null) {
errors['dob'] = 'Invalid date format';
} else if (dob.isAfter(DateTime.now())) {
errors['dob'] = 'Date of Birth cannot be in the future';
}
}
setState(() {});
return errors.isEmpty;
}
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 _buildTextField(
TextEditingController controller,
String label,
IconData icon,
String errorKey,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controller,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(
icon,
color: errors.containsKey(errorKey) ? Colors.red : Colors.blue,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color:
errors.containsKey(errorKey) ? Colors.red : Colors.grey,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
errors.containsKey(errorKey) ? Colors.red : Colors.grey,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
errors.containsKey(errorKey) ? Colors.red : Colors.blue,
),
),
),
),
if (errors.containsKey(errorKey))
Padding(
padding: const EdgeInsets.only(top: 4, left: 12),
child: Text(
errors[errorKey]!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
),
);
}
List<Widget> _buildAppBarActions() {
return [
TextButton(
onPressed: () {
if (_validateFields()) {
FamilyMember newMember = FamilyMember(
name: nameController.text,
relation: relationController.text,
gender: genderController.text,
dateOfBirth: DateTime.tryParse(dobController.text),
);
Navigator.pop(context, newMember);
} else {
_showValidationErrors();
}
},
child: const Text('Done', style: TextStyle(color: Colors.blue)),
),
];
}
@override
void dispose() {
nameController.dispose();
relationController.dispose();
genderController.dispose();
dobController.dispose();
super.dispose();
}
}

View File

@ -1,381 +0,0 @@
import 'package:medora/controllers/patient_controller.dart';
import 'package:flutter/material.dart';
import 'package:country_state_city_picker/country_state_city_picker.dart';
class PatientAddressScreen extends StatefulWidget {
final PatientController? controller;
const PatientAddressScreen({super.key, required this.controller});
@override
State<PatientAddressScreen> createState() => _PatientAddressScreenState();
}
class _PatientAddressScreenState extends State<PatientAddressScreen> {
late PatientController _controller;
late TextEditingController _houseNoController;
late TextEditingController _lineController;
late TextEditingController _townController;
late TextEditingController _pincodeController;
late TextEditingController _otherLabelController;
final String country = 'India';
String? state;
String? city;
String? addressType;
final Map<String, String> _errors = {};
bool _hasErrors = false;
@override
void initState() {
super.initState();
_controller = widget.controller ?? PatientController();
_loadSavedData();
}
void _loadSavedData() {
final address = _controller.model.address;
_houseNoController = TextEditingController(text: address.houseNo ?? '');
_lineController = TextEditingController(text: address.line ?? '');
_townController = TextEditingController(text: address.town ?? '');
_pincodeController = TextEditingController(text: address.pincode ?? '');
_otherLabelController =
TextEditingController(text: address.otherLabel ?? '');
state = address.state;
city = address.city;
addressType = address.addressType;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Address'),
actions: [
TextButton(
onPressed: _saveAndExit,
child: const Text('Done', style: TextStyle(color: Colors.blue)),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionContainer(
'Address Information',
Column(
children: [
_buildTextField(
'House No.',
_houseNoController,
(value) => widget.controller!.updateHouseNo(value),
icon: Icons.home_outlined,
errorKey: 'houseNo',
),
_buildTextField(
'Address Line',
_lineController,
(value) => widget.controller!.updateLine(value),
icon: Icons.location_on_outlined,
errorKey: 'line',
),
_buildTextField(
'Town (Optional)',
_townController,
(value) => widget.controller!.updateTown(value),
icon: Icons.location_city_outlined,
),
_buildTextField(
'Pincode',
_pincodeController,
(value) => widget.controller!.updatePincode(value),
icon: Icons.pin_drop_outlined,
errorKey: 'pincode',
),
],
),
),
const SizedBox(height: 20),
_buildSectionContainer(
'Location',
Column(
children: [
_buildCountrySelection(),
const SizedBox(height: 10),
SelectState(
onCountryChanged: (value) {
setState(() {});
widget.controller!.updateCountry('India');
},
onStateChanged: (value) {
setState(() {
state = value;
});
widget.controller!.updateState(value);
},
onCityChanged: (value) {
setState(() {
city = value;
});
widget.controller!.updateCity(value);
},
),
const SizedBox(height: 20),
if (state != null)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text('State: $state',
style: const TextStyle(
fontSize: 14, color: Colors.black87)),
),
if (city != null)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text('City: $city',
style: const TextStyle(
fontSize: 14, color: Colors.black87)),
),
],
),
),
const SizedBox(height: 20),
_buildSectionContainer(
'Address Type',
Column(
children: [
_buildAddressTypeChips(),
if (addressType == 'Other')
_buildTextField(
'Other Label',
_otherLabelController,
(value) => widget.controller!.updateOtherLabel(value),
icon: Icons.label_outline,
),
],
),
),
],
),
),
);
}
bool _validateFields() {
setState(() {
_errors.clear();
_hasErrors = false;
if (_houseNoController.text.trim().isEmpty) {
_errors['houseNo'] = 'House No. is required';
_hasErrors = true;
}
if (_lineController.text.trim().isEmpty) {
_errors['line'] = 'Address Line is required';
_hasErrors = true;
}
final pincode = _pincodeController.text.trim();
if (pincode.isEmpty) {
_errors['pincode'] = 'Pincode is required';
_hasErrors = true;
} else if (!RegExp(r'^\d{6}$').hasMatch(pincode)) {
_errors['pincode'] = 'Enter a valid 6-digit pincode';
_hasErrors = true;
}
if (state == null || state!.isEmpty) {
_errors['state'] = 'State is required';
_hasErrors = true;
}
if (city == null || city!.isEmpty) {
_errors['city'] = 'City is required';
_hasErrors = true;
}
if (addressType == null || addressType!.isEmpty) {
_errors['addressType'] = 'Please select an address type';
_hasErrors = true;
}
if (addressType == 'Other' && _otherLabelController.text.trim().isEmpty) {
_errors['otherLabel'] = 'Please specify other label';
_hasErrors = true;
}
});
return !_hasErrors;
}
Widget _buildSectionContainer(String title, Widget content) {
return Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.5),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
content,
],
),
);
}
Widget _buildTextField(
String label,
TextEditingController controller,
Function(String) onChanged, {
required IconData icon,
String? errorKey,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controller,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon,
color: _errors.containsKey(errorKey)
? Colors.red
: Colors.blueAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: _errors.containsKey(errorKey)
? Colors.red
: Colors.blueAccent,
),
),
errorText: _errors[errorKey],
),
onChanged: onChanged,
),
const SizedBox(height: 20),
],
);
}
Widget _buildCountrySelection() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: const Row(
children: [
Text(
'Country:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(width: 8),
Text('India', style: TextStyle(fontSize: 16)),
],
),
);
}
Widget _buildAddressTypeChips() {
return Wrap(
spacing: 8.0,
children: ['Home', 'Office', 'Other'].map((String type) {
return ChoiceChip(
label: Text(type),
selected: addressType == type,
onSelected: (bool selected) {
setState(() {
addressType = selected ? type : addressType;
});
widget.controller!.updateAddressType(addressType!);
},
);
}).toList(),
);
}
void _saveAndExit() {
if (_validateFields()) {
widget.controller!.updateHouseNo(_houseNoController.text);
widget.controller!.updateLine(_lineController.text);
widget.controller!.updateTown(_townController.text);
widget.controller!.updatePincode(_pincodeController.text);
widget.controller!.updateCountry(country);
widget.controller!.updateState(state ?? '');
widget.controller!.updateCity(city ?? '');
widget.controller!.updateAddressType(addressType ?? '');
widget.controller!.updateOtherLabel(_otherLabelController.text);
widget.controller!.updatePatientData();
Navigator.pop(context, true);
} else {
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'),
),
],
),
);
}
}
@override
void dispose() {
_houseNoController.dispose();
_lineController.dispose();
_townController.dispose();
_pincodeController.dispose();
_otherLabelController.dispose();
super.dispose();
}
}

View File

@ -1,308 +0,0 @@
import 'package:medora/data/models/patient.dart';
import 'package:flutter/material.dart';
import 'package:medora/screens/patient_screens/registration_screens/family_members_edit_screen.dart';
import '../../../controllers/patient_controller.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class PatientFamilyMembersScreen extends StatefulWidget {
final PatientController controller;
const PatientFamilyMembersScreen({
super.key,
required this.controller,
});
@override
State<PatientFamilyMembersScreen> createState() =>
_PatientFamilyMembersScreenState();
}
class _PatientFamilyMembersScreenState
extends State<PatientFamilyMembersScreen> {
bool isLoading = false;
final int maxFamilyMembers = 5;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Family Members',
style: TextStyle(fontSize: 20),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () {
if (_validateFamilyMembers()) {
Navigator.pop(context);
}
},
),
actions: _buildAppBarActions(),
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: widget.controller.model.familyMembers.length,
itemBuilder: (context, index) {
return FamilyMemberCard(
familyMember: widget.controller.model.familyMembers[index],
onEdit: () => _editFamilyMember(index),
onDelete: () => _deleteFamilyMember(index),
);
},
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _addFamilyMember,
backgroundColor: Colors.blue,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
List<Widget> _buildAppBarActions() {
return [
TextButton(
onPressed: () {
if (_validateFamilyMembers()) {
Navigator.pop(context);
}
},
child: const Text(
'Done',
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
),
),
];
}
bool _validateFamilyMembers() {
if (widget.controller.model.familyMembers.isEmpty) {
_showValidationError('Please add at least one family member');
return false;
}
final relations = widget.controller.model.familyMembers
.map((member) => member.relation?.toLowerCase())
.toList();
if (relations.toSet().length != relations.length) {
_showValidationError('Duplicate relations are not allowed');
return false;
}
return true;
}
void _showValidationError(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Validation Error'),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
void _addFamilyMember() {
if (widget.controller.model.familyMembers.length >= maxFamilyMembers) {
_showValidationError('Maximum $maxFamilyMembers family members allowed');
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FamilyMembersEditScreen(
controller: widget.controller,
),
),
).then((newMember) {
if (newMember != null) {
setState(() {
widget.controller.addFamilyMember(newMember);
});
}
});
}
void _editFamilyMember(int index) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FamilyMembersEditScreen(
controller: widget.controller,
familyMember: widget.controller.model.familyMembers[index],
),
),
).then((editedMember) {
if (editedMember != null) {
setState(() {
widget.controller.updateFamilyMember(index, editedMember);
});
}
});
}
void _deleteFamilyMember(int index) {
if (widget.controller.model.familyMembers.length <= 1) {
_showValidationError('At least one family member is required');
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Family Member'),
content:
const Text('Are you sure you want to delete this family member?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
setState(() {
widget.controller.deleteFamilyMember(index);
});
Navigator.pop(context);
},
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}
class FamilyMemberCard extends StatelessWidget {
final FamilyMember familyMember;
final VoidCallback onEdit;
final VoidCallback onDelete;
const FamilyMemberCard({
super.key,
required this.familyMember,
required this.onEdit,
required this.onDelete,
});
Widget _buildInfoRow(IconData icon, String label, String? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.blueGrey),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
value ?? 'Not provided',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: value == null || value.isEmpty
? Colors.redAccent
: Colors.black87,
fontStyle: value == null || value.isEmpty
? FontStyle.italic
: FontStyle.normal,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Slidable(
key: ValueKey(familyMember),
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.3,
children: [
SlidableAction(
onPressed: (context) => onEdit(),
foregroundColor: Colors.blue,
icon: Icons.edit,
padding: EdgeInsets.zero,
spacing: 0,
),
SlidableAction(
onPressed: (context) => onDelete(),
foregroundColor: Colors.red,
icon: Icons.delete,
padding: EdgeInsets.zero,
spacing: 0,
),
],
),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.blueGrey[50],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildInfoRow(Icons.person, 'Name:', familyMember.name),
const SizedBox(height: 10),
_buildInfoRow(
Icons.transgender, 'Gender:', familyMember.gender),
const SizedBox(height: 10),
_buildInfoRow(
Icons.cake,
'Date of Birth:',
familyMember.dateOfBirth?.toString().split(' ')[0] ??
'Not provided',
),
const SizedBox(height: 10),
_buildInfoRow(
Icons.family_restroom, 'Relation:', familyMember.relation),
],
),
),
),
),
);
}
}

View File

@ -1,680 +0,0 @@
import 'package:medora/route/route_names.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import '../../../controllers/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;
}
final address = _controller.model.address;
if (address.houseNo?.isEmpty ?? true) {
_errors['address'] = 'Please complete all required address fields';
_hasErrors = true;
}
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),
);
}
}

View File

@ -20,9 +20,8 @@ class _TelemednetAppState extends State<TelemednetApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
initialRoute: RouteNames.splashScreen, initialRoute: RouteNames.signIn,
routes: { routes: {
RouteNames.splashScreen: (context) => const SplashScreen(),
...routes, ...routes,
}, },
); );

View File

@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.0"
country_state_city: country_state_city:
dependency: "direct main" dependency: "direct main"
description: description:
@ -423,10 +423,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_slidable name: flutter_slidable
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" sha256: ab7dbb16f783307c9d7762ede2593ce32c220ba2ba0fd540a3db8e9a3acba71a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "4.0.0"
flutter_staggered_animations: flutter_staggered_animations:
dependency: "direct main" dependency: "direct main"
description: description:
@ -601,18 +601,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "10.0.7"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.8"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -761,7 +761,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -782,10 +782,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -798,10 +798,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -814,10 +814,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.3"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -870,10 +870,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.3.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@ -915,5 +915,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.5.3 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.27.0"

View File

@ -47,7 +47,7 @@ dependencies:
country_state_city: ^0.1.6 country_state_city: ^0.1.6
country_state_city_picker: ^1.2.8 country_state_city_picker: ^1.2.8
intl_phone_field: ^3.2.0 intl_phone_field: ^3.2.0
flutter_slidable: ^3.1.1 flutter_slidable: ^4.0.0
gap: ^3.0.1 gap: ^3.0.1
curved_navigation_bar: ^1.0.6 curved_navigation_bar: ^1.0.6
google_fonts: ^6.2.1 google_fonts: ^6.2.1