Split the buisiness logic, to keep only doctor related buisiness
This commit is contained in:
parent
54f05e377d
commit
370107db72
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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'];
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:medora/data/models/telemed_user.dart';
|
||||
import 'package:medora/data/services/data_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';
|
||||
|
||||
class NavigationService {
|
||||
@ -17,24 +16,16 @@ class NavigationService {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (userProfile.role.toLowerCase()) {
|
||||
case 'doctor':
|
||||
if (context.mounted) {
|
||||
handleDoctorNavigation(context);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'patient':
|
||||
if (context.mounted) {
|
||||
handlePatientNavigation(context);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
if (userProfile.role.toLowerCase() != 'doctor') {
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(context, RouteNames.launch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
await handleDoctorNavigation(context);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error in handleUserNavigation: $e');
|
||||
if (context.mounted) {
|
||||
@ -57,25 +48,25 @@ class NavigationService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> handlePatientNavigation(BuildContext context) async {
|
||||
try {
|
||||
final patientProfile = await PatientProfileService.getPatientProfile();
|
||||
// static Future<void> handlePatientNavigation(BuildContext context) async {
|
||||
// try {
|
||||
// final patientProfile = await PatientProfileService.getPatientProfile();
|
||||
|
||||
if (context.mounted) {
|
||||
if (patientProfile != null) {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, RouteNames.patientDashboardScreen);
|
||||
} else {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, RouteNames.patientLandingScreen);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error in handlePatientNavigation: $e');
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(
|
||||
context, RouteNames.patientLandingScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (context.mounted) {
|
||||
// if (patientProfile != null) {
|
||||
// Navigator.pushReplacementNamed(
|
||||
// context, RouteNames.patientDashboardScreen);
|
||||
// } else {
|
||||
// Navigator.pushReplacementNamed(
|
||||
// context, RouteNames.patientLandingScreen);
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('Error in handlePatientNavigation: $e');
|
||||
// if (context.mounted) {
|
||||
// Navigator.pushReplacementNamed(
|
||||
// context, RouteNames.patientLandingScreen);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:telemednet/telemed_user.dart';
|
||||
import 'package:medora/data/models/telemed_user.dart';
|
||||
|
||||
class DataService {
|
||||
static final String profileCollectionName =
|
||||
|
||||
@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:medora/controllers/consultation_center_controller.dart';
|
||||
import 'package:medora/data/models/consultation_center.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/data/telemed_user.dart';
|
||||
import 'package:medora/controllers/patient_controller.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/doctor_screen/doctor_consultation_schedule/business_center_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/qualifications_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 '../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 = {
|
||||
RouteNames.launch: (context) => const LaunchScreen(),
|
||||
// RouteNames.launch: (context) => const LaunchScreen(),
|
||||
RouteNames.signIn: (context) => SignInScreen(
|
||||
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",
|
||||
),
|
||||
RouteNames.signUp: (context) => const RegisterScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
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 SignUpScreen(),
|
||||
// RouteNames.userProfile: (context) {
|
||||
// var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?;
|
||||
// return UserProfileScreen(user: user);
|
||||
// },
|
||||
// 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.patientRegistrationScreen: (context) =>
|
||||
const PatientRegistrationScreen(),
|
||||
RouteNames.qualificationsScreen: (context) {
|
||||
final controller =
|
||||
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
||||
@ -68,6 +72,7 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
||||
controller: controller ?? DoctorController(), // Provide fallback
|
||||
);
|
||||
},
|
||||
RouteNames.profileUpload: (context) => const ProfileUploadPage(),
|
||||
RouteNames.doctorAddressScreen: (context) {
|
||||
final controller =
|
||||
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
||||
@ -110,65 +115,6 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
||||
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.doctorHomeScreen: (context) => const DoctorDashboardHomeScreen(),
|
||||
RouteNames.doctorPersonalProfileScreen: (context) =>
|
||||
|
||||
@ -185,9 +185,7 @@ class _LaunchScreenState extends State<LaunchScreen> {
|
||||
void _navigateToSignUp() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SignUpScreen(
|
||||
selectedUserType: selectedUserType!,
|
||||
),
|
||||
builder: (context) => const SignUpScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,11 +7,11 @@ import 'package:medora/data/services/navigation_service.dart';
|
||||
import 'package:medora/widgets/primary_button.dart';
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
final String selectedUserType;
|
||||
// final String selectedUserType;
|
||||
|
||||
const SignUpScreen({
|
||||
super.key,
|
||||
required this.selectedUserType,
|
||||
// required this.selectedUserType,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -160,7 +160,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Register as ${widget.selectedUserType}',
|
||||
'Register as doctor',
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@ -299,17 +299,13 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
final result = await DataService.createUserProfile(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
userType: widget.selectedUserType,
|
||||
userType: 'doctor',
|
||||
phoneNumber: _completePhoneNumber,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
if (result['success']) {
|
||||
if (widget.selectedUserType.toLowerCase() == 'doctor') {
|
||||
await NavigationService.handleDoctorNavigation(context);
|
||||
} else {
|
||||
await NavigationService.handlePatientNavigation(context);
|
||||
}
|
||||
} else {
|
||||
_showErrorSnackBar(result['message']);
|
||||
}
|
||||
|
||||
@ -187,7 +187,7 @@ class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
||||
try {
|
||||
await _auth.signOut();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed(RouteNames.launch);
|
||||
Navigator.of(context).pushReplacementNamed(RouteNames.signIn);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error signing out: $e");
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -20,9 +20,8 @@ class _TelemednetAppState extends State<TelemednetApp> {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
initialRoute: RouteNames.splashScreen,
|
||||
initialRoute: RouteNames.signIn,
|
||||
routes: {
|
||||
RouteNames.splashScreen: (context) => const SplashScreen(),
|
||||
...routes,
|
||||
},
|
||||
);
|
||||
|
||||
38
pubspec.lock
38
pubspec.lock
@ -125,10 +125,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
country_state_city:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -423,10 +423,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
|
||||
sha256: ab7dbb16f783307c9d7762ede2593ce32c220ba2ba0fd540a3db8e9a3acba71a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "4.0.0"
|
||||
flutter_staggered_animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -601,18 +601,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -761,7 +761,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -782,10 +782,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -798,10 +798,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -814,10 +814,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -870,10 +870,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -915,5 +915,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.3 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
||||
@ -47,7 +47,7 @@ dependencies:
|
||||
country_state_city: ^0.1.6
|
||||
country_state_city_picker: ^1.2.8
|
||||
intl_phone_field: ^3.2.0
|
||||
flutter_slidable: ^3.1.1
|
||||
flutter_slidable: ^4.0.0
|
||||
gap: ^3.0.1
|
||||
curved_navigation_bar: ^1.0.6
|
||||
google_fonts: ^6.2.1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user