diff --git a/.env b/.env index e1a1315..f6cc2cf 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ CUSTOM_SCHEME=com.cosqnet.telemednet PROFILE_COLLECTION_NAME=telemednetusers +PATIENT_PROFILE_COLLECTION_NAME=patientprofiles +DOCTOR_PROFILE_COLLECTION_NAME=doctorprofiles \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 78015e5..e25bdfe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -16,7 +16,7 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { namespace = "com.cosqnet.telemednet" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "25.1.8937393" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -28,10 +28,7 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.cosqnet.telemednet" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode diff --git a/android/settings.gradle b/android/settings.gradle index 9759a22..7832f56 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,10 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - // START: FlutterFire Configuration + id "com.android.application" version "8.3.2" apply false id "com.google.gms.google-services" version "4.3.15" apply false - // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/lib/common/color_scheme.dart b/lib/common/color_scheme.dart new file mode 100644 index 0000000..53f06df --- /dev/null +++ b/lib/common/color_scheme.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +const lightColorScheme = ColorScheme( + brightness: Brightness.light, + primary: Colors.blue, // blue color + onPrimary: Colors.white, + secondary: Color(0xFFB94E48), // Reddish-brown accent color + onSecondary: Colors.white, + error: Color(0xFFB94E48), // Using the same reddish-brown for error + onError: Colors.white, // Dark grey for text/elements + surface: Color(0xFFE0E0E0), // Light grey for surfaces + onSurface: Color(0xFF333333), // Dark grey for text/elements on surfaces +); + +const darkColorScheme = ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFFAC9B8C), // Desaturated tan/beige color + onPrimary: Colors.black, + secondary: Color(0xFFD57C73), // Reddish-brown accent color + onSecondary: Colors.black, + error: Color(0xFFD57C73), // Using the same reddish-brown for error + onError: Colors.black, // Light cream for text/elements + surface: Color(0xFF4D4D4D), // Darker grey for surfaces + onSurface: Color(0xFFF5F5F5), // Light cream for text/elements on surfaces +); diff --git a/lib/common/custom_style.dart b/lib/common/custom_style.dart new file mode 100644 index 0000000..4940d9b --- /dev/null +++ b/lib/common/custom_style.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; + +class CustomStyles { + final BuildContext context; + final ButtonStyle primaryButtonStyle; + final ButtonStyle secondaryButtonStyle; + + static const double gutter = 24; + static const double smallGutter = 12; + static const double pagePadding = 16; + + static const double snackBarMargin = 16; + static const double snackBarBorderRadius = 8; + static const double horizontalPadding = 24; + static const double minimumButtonHeight = 46; + static const double minimumButtonWidth = 92; + static const double verticalPadding = 12; + static const double defaultButtonCornerRadius = 6; + static const double defaultInputFieldCornerRadius = 6; + static const double defaultInputContentPadding = 12; + static const double paddingLarge = 32; + static const EdgeInsets listPadding = EdgeInsets.all(12); + static const double defaultMarginVertical = 4; + static const double defaultMarginHorizontal = 5; + static const double defaultCardCornerRadius = 12; + static const double defaultContentPaddingHorizontal = 8; + static const double defaultContentPadding = 12; + static const double defaultWrapSpacing = 4; + static const double defaultCornerRadious = 8; + static const double defaultBottomAppbarPreferedSize = 4; + static const int defaultNumberOfAdults = 1; + static const int defaultNumberOfMaxResults = 10; + static const double defaultBottomPadding = 16; + static const double defaultCardElevation = 3; + static const double defaultAppBarElevation = 4; + static const int defaultmaxLines = 2; + static const double defaultlineXY = 0.23; + static const double defaultRightPadding = 16; + static const double defaultSizedBoxWidth = 8; + static const double defaultMapZoom = 16; + static const int defaultPolylineWidth = 3; + static const double defaultCameraUpdatePadding = 100; + static const int defaultPageControllerDuration = 300; + static const double defaultCarouselOptionsAspectRatio = 16 / 9; + static const double defaultPageMaxWidth = 360; + static const double packageCoverimageHeightRatio = .18; + static const double coverImaageHeightRatio = 0.25; + static const double itenaryImageHeightRatio = 0.20; + static const double nonSightseeingMapHeightRatio = 0.4; + static const double chatCardMaxWidthRatio = 0.7; + + CustomStyles({required this.context}) + : primaryButtonStyle = ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(defaultButtonCornerRadius), + ), + ), + secondaryButtonStyle = OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(defaultButtonCornerRadius), + ), + ); +} + +void showErrorSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(CustomStyles.snackBarMargin), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(CustomStyles.snackBarBorderRadius), + ), + ), + ); +} + +void showSuccessSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(CustomStyles.snackBarMargin), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(CustomStyles.snackBarBorderRadius), + ), + ), + ); +} + +void showInfoSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.blue, + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(CustomStyles.snackBarMargin), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(CustomStyles.snackBarBorderRadius), + ), + ), + ); +} + +class Gutter extends Gap { + const Gutter({super.key}) : super(CustomStyles.gutter); +} + +class SmallGutter extends Gap { + const SmallGutter({super.key}) : super(CustomStyles.smallGutter); +} + +class ListTileColor { + static Color of(BuildContext context, int index) { + return index.isOdd + ? Theme.of(context).colorScheme.surface.withAlpha(255) + : Theme.of(context).colorScheme.surface.withAlpha(125); + } +} diff --git a/lib/controller/patient_controller.dart b/lib/controller/patient_controller.dart new file mode 100644 index 0000000..e61595a --- /dev/null +++ b/lib/controller/patient_controller.dart @@ -0,0 +1,104 @@ +import 'package:telemednet/data/models/patient.dart'; +import '../data/services/patient_registration_service.dart'; + +class PatientController { + final PatientModel model = PatientModel(); + Map 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 savePatientData() async { + return await PatientProfileService.savePatientProfile(this); + } + + Future loadPatientData() async { + PatientModel? loadedModel = await PatientProfileService.getPatientProfile(); + if (loadedModel != null) { + model.updateFrom(loadedModel); + return true; + } + return false; + } + + Future updatePatientData() async { + return await PatientProfileService.updatePatientProfile(this); + } + + Future deletePatientData() async { + return await PatientProfileService.deletePatientProfile(); + } +} diff --git a/lib/controllers/doctor _controller.dart b/lib/controllers/doctor _controller.dart new file mode 100644 index 0000000..8f49dd2 --- /dev/null +++ b/lib/controllers/doctor _controller.dart @@ -0,0 +1,115 @@ +import 'package:telemednet/data/models/doctor.dart'; + +class DoctorController { + Doctor model = Doctor( + address: Address(floorBuilding: ''), + profile: Profile(qualifications: []), + ); + + final ProfileController profileController = ProfileController(); + final AddressController addressController = AddressController(); + + // Doctor Specific Updates + void updateSpeciality(String speciality) { + model.speciality = speciality; + } + + void updateAchievements(List achievements) { + model.achievements = achievements; + } + + // Add single achievement + void addAchievement(String achievement) { + model.achievements.add(achievement); + } + + // Remove achievement + void removeAchievement(String achievement) { + model.achievements.remove(achievement); + } + + void updateYearsOfExperience(String years) { + model.yearsOfExperience = years; + } + + void updateLicenseNumber(String license) { + model.licenseNumber = license; + } + + void updateProfileDescription(String description) { + model.profileDescription = description; + } + + void updateDigitalSignature(String signature) { + model.digitalSignature = signature; + } + + // Validation logic can be added here + bool validateProfile() { + return model.speciality != null && model.licenseNumber != null; + } +} + +class ProfileController { + Profile model = Profile(qualifications: []); + + void updateTitle(String title) { + model.title = title; + } + + void updateSurName(String surName) { + model.surName = surName; + } + + void updateMiddleName(String middleName) { + model.middleName = middleName; + } + + void updateLastName(String lastName) { + model.lastName = lastName; + } + + void addQualification(String qualification) { + model.qualifications.add(qualification); + } + + void removeQualification(String qualification) { + model.qualifications.remove(qualification); + } + + bool validateProfile() { + return model.qualifications.isNotEmpty; + } +} + +class AddressController { + Address model = Address(floorBuilding: ''); + + void updateFloorBuilding(String floorBuilding) { + model.floorBuilding = floorBuilding; + } + + void updateStreet(String street) { + model.street = street; + } + + void updateCity(String city) { + model.city = city; + } + + void updateState(String state) { + model.state = state; + } + + void updateCountry(String country) { + model.country = country; + } + + void updatePostalCode(String postalCode) { + model.postalCode = postalCode; + } + + // bool validateAddress() { + // return model.floorBuilding.isNotEmpty; + // } +} diff --git a/lib/data/data_service.dart b/lib/data/data_service.dart deleted file mode 100644 index 54b6bea..0000000 --- a/lib/data/data_service.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:telemednet/data/models/telemed_user.dart'; - -class DataService { - static final String profileCollectionName = - dotenv.env['PROFILE_COLLECTION_NAME']!; - static final db = FirebaseFirestore.instance; - - static User? getCurrentUser() { - return FirebaseAuth.instance.currentUser; - } - - static Future getProfile() async { - try { - final user = getCurrentUser(); - if (user == null) { - return null; - } - - final uid = user.uid; - final profiles = db.collection(profileCollectionName); - final profile = await profiles.doc(uid).get(); - if (!profile.exists) { - return null; - } - final profileData = profile.data(); - if (profileData == null) { - return null; - } - var telemedUser = TelemedUser.fromJson(profileData, uid); - return telemedUser; - } catch (e) { - print(e); - return null; - } - } -} diff --git a/lib/data/models/doctor.dart b/lib/data/models/doctor.dart new file mode 100644 index 0000000..a075ace --- /dev/null +++ b/lib/data/models/doctor.dart @@ -0,0 +1,116 @@ +class Doctor { + // Add achievements field + List achievements; + + // Add to existing fields + String? speciality; + String? yearsOfExperience; + String? licenseNumber; + String? profileDescription; + String? digitalSignature; + Address address; + Profile profile; + + Doctor({ + this.achievements = const [], // Initialize with empty list + this.speciality, + this.yearsOfExperience, + this.licenseNumber, + this.profileDescription, + this.digitalSignature, + required this.address, + required this.profile, + }); + + Map toJson() => { + 'achievements': achievements, + 'speciality': speciality, + 'yearsOfExperience': yearsOfExperience, + 'licenseNumber': licenseNumber, + 'profileDescription': profileDescription, + 'digitalSignature': digitalSignature, + 'address': address.toJson(), + 'profile': profile.toJson(), + }; + + static Doctor fromJson(Map json) => Doctor( + achievements: List.from(json['achievements'] ?? []), + speciality: json['speciality'], + yearsOfExperience: json['yearsOfExperience'], + licenseNumber: json['licenseNumber'], + profileDescription: json['profileDescription'], + digitalSignature: json['digitalSignature'], + address: Address.fromJson(json['address']), + profile: Profile.fromJson(json['profile']), + ); +} + +class Profile { + String? title; + String? surName; + String? middleName; + String? lastName; + List qualifications; + + var profileDescription; + + Profile({ + this.title, + this.surName, + this.middleName, + this.lastName, + required this.qualifications, + }); + + Map toJson() => { + 'title': title, + 'surname': surName, + 'lastName': lastName, + 'middleName': middleName, + 'qualifications': qualifications, + }; + + static Profile fromJson(Map json) => Profile( + title: json['title'], + surName: json['surname'], + middleName: json['middleName'], + lastName: json['lastName'], + qualifications: List.from(json['qualifications']), + ); +} + +class Address { + String? floorBuilding; + String? street; + String? city; + String? state; + String? country; + String? postalCode; + + Address({ + this.floorBuilding, + this.street, + this.city, + this.state, + this.country, + this.postalCode, + }); + + Map toJson() => { + 'floorBuilding': floorBuilding, + 'street': street, + 'city': city, + 'state': state, + 'country': country, + 'postalCode': postalCode, + }; + + static Address fromJson(Map json) => Address( + floorBuilding: json['floorBuilding'], + street: json['street'], + city: json['city'], + state: json['state'], + country: json['country'], + postalCode: json['postalCode'], + ); +} diff --git a/lib/data/models/patient.dart b/lib/data/models/patient.dart index 8b13789..0d93c62 100644 --- a/lib/data/models/patient.dart +++ b/lib/data/models/patient.dart @@ -1 +1,144 @@ +class PatientModel { + String? name; + String? phoneNumber; + String? gender; + DateTime? dateOfBirth; + String? profileImagePath; + PatientAddress address; + List familyMembers = []; + + PatientModel() : address = PatientAddress(); + + Map toJson() { + return { + 'name': name, + 'phoneNumber': phoneNumber, + 'gender': gender, + 'dateOfBirth': dateOfBirth?.toIso8601String(), + 'profileImagePath': profileImagePath, + 'address': address.toJson(), + 'familyMembers': familyMembers.map((member) => member.toJson()).toList(), + }; + } + + PatientModel.fromJson(Map json) + : address = PatientAddress() { + name = json['name']; + phoneNumber = json['phoneNumber']; + gender = json['gender']; + dateOfBirth = json['dateOfBirth'] != null + ? DateTime.parse(json['dateOfBirth']) + : null; + profileImagePath = json['profileImagePath']; + address.houseNo = json['houseNo']; + address.line = json['line']; + address.town = json['town']; + address.pincode = json['pincode']; + address.country = json['country']; + address.state = json['state']; + address.city = json['city']; + address.addressType = json['addressType']; + address.otherLabel = json['otherLabel']; + 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; + address.houseNo = other.address.houseNo; + address.line = other.address.line; + address.town = other.address.town; + address.pincode = other.address.pincode; + address.country = other.address.country; + address.state = other.address.state; + address.city = other.address.city; + address.addressType = other.address.addressType; + address.otherLabel = other.address.otherLabel; + familyMembers = other.familyMembers; + } +} + +class FamilyMember { + String? name; + String? relation; + String? gender; + DateTime? dateOfBirth; + + FamilyMember({this.name, this.relation, this.gender, this.dateOfBirth}); + + Map toJson() { + return { + 'name': name, + 'relation': relation, + 'gender': gender, + 'dateOfBirth': dateOfBirth?.toIso8601String(), + }; + } + + FamilyMember.fromJson(Map 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 toJson() { + return { + 'houseNo': houseNo, + 'line': line, + 'town': town, + 'pincode': pincode, + 'country': country, + 'state': state, + 'city': city, + 'addressType': addressType, + 'otherLabel': otherLabel, + }; + } + + PatientAddress.fromJson(Map 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']; + } +} diff --git a/lib/data/models/telemed_user.dart b/lib/data/models/telemed_user.dart index ed68a07..3041863 100644 --- a/lib/data/models/telemed_user.dart +++ b/lib/data/models/telemed_user.dart @@ -1,41 +1,29 @@ -enum UserRole { doctor, patient } - class TelemedUser { - String uid; - late String? name; - late String? email; - late String? photoURL; - late String? phoneNumber; - late String? alterPhoneNumber; - late UserRole? role; - TelemedUser( - {required this.uid, - this.name, - this.email, - this.photoURL, - this.phoneNumber, - this.alterPhoneNumber, - this.role}); + late String uid; + String? email; + String? phoneNumber; + late String role; - TelemedUser.fromJson(Map json, this.uid) { - uid = json['uid']; - name = json['name']; - email = json['email']; - photoURL = json['photoURL']; - phoneNumber = json['phoneNumber']; - alterPhoneNumber = json['alterPhoneNumber']; - role = json['role']; + TelemedUser({ + required this.uid, + this.email, + this.phoneNumber, + required this.role, + }); + + TelemedUser.fromJson(Map json, String userId) { + uid = userId; + email = json['email'] as String?; + phoneNumber = json['phoneNumber'] as String?; + role = (json['UserType'] ?? json['role']) as String; } Map toJson() { - final Map data = {}; - data['uid'] = uid; - data['name'] = name; - data['email'] = email; - data['photoURL'] = photoURL; - data['phoneNumber'] = phoneNumber; - data['alterPhoneNumber'] = alterPhoneNumber; - data['role'] = role; - return data; + return { + 'uid': uid, + 'email': email, + 'phoneNumber': phoneNumber, + 'UserType': role, + }; } } diff --git a/lib/data/services/data_service.dart b/lib/data/services/data_service.dart new file mode 100644 index 0000000..37fe338 --- /dev/null +++ b/lib/data/services/data_service.dart @@ -0,0 +1,92 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:telemednet/data/models/telemed_user.dart'; + +class DataService { + static final String profileCollectionName = + dotenv.env['PROFILE_COLLECTION_NAME']!; + static final db = FirebaseFirestore.instance; + static final auth = FirebaseAuth.instance; + + static Future> createUserProfile({ + required String email, + required String password, + required String userType, + String? phoneNumber, + }) async { + try { + final UserCredential userCredential = + await auth.createUserWithEmailAndPassword( + email: email, + password: password, + ); + + final User? user = userCredential.user; + if (user == null) { + return {'success': false, 'message': 'Failed to create user account'}; + } + + final profiles = db.collection(profileCollectionName); + await profiles.doc(user.uid).set({ + 'UserType': userType, + 'email': email, + 'phoneNumber': phoneNumber, + 'createdAt': FieldValue.serverTimestamp(), + }); + + return {'success': true, 'message': 'Account created successfully'}; + } on FirebaseAuthException catch (e) { + String message; + switch (e.code) { + case 'email-already-in-use': + message = + 'This email is already registered. Please use a different email or sign in.'; + break; + case 'invalid-email': + message = 'The email address is invalid. Please check and try again.'; + break; + case 'operation-not-allowed': + message = + 'Email/password accounts are not enabled. Please contact support.'; + break; + case 'weak-password': + message = 'The password is too weak. Please use a stronger password.'; + break; + default: + message = 'An error occurred during registration: ${e.message}'; + } + return {'success': false, 'message': message}; + } on FirebaseException catch (e) { + return {'success': false, 'message': 'Database error: ${e.message}'}; + } catch (e) { + return { + 'success': false, + 'message': 'An unexpected error occurred. Please try again.' + }; + } + } + + static User? getCurrentUser() { + return auth.currentUser; + } + + static Future getProfile() async { + try { + final user = getCurrentUser(); + if (user == null) return null; + + final profile = + await db.collection(profileCollectionName).doc(user.uid).get(); + if (!profile.exists) return null; + + final profileData = profile.data(); + if (profileData == null) return null; + + return TelemedUser.fromJson(profileData, user.uid); + } catch (e) { + print('Error getting profile: $e'); + return null; + } + } +} diff --git a/lib/data/services/doctor_profile_service.dart b/lib/data/services/doctor_profile_service.dart new file mode 100644 index 0000000..8349f78 --- /dev/null +++ b/lib/data/services/doctor_profile_service.dart @@ -0,0 +1,118 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:telemednet/data/models/doctor.dart'; + +import '../../controllers/doctor _controller.dart'; + +class DoctorProfileService { + static final String doctorProfileCollectionName = + dotenv.env['DOCTOR_PROFILE_COLLECTION_NAME']!; + static final FirebaseFirestore _db = FirebaseFirestore.instance; + + static Future saveDoctorProfile(DoctorController 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 Doctor doctorData = controller.model; + + final Map doctorJson = doctorData.toJson(); + doctorJson['createdAt'] = FieldValue.serverTimestamp(); + doctorJson['updatedAt'] = FieldValue.serverTimestamp(); + doctorJson['uid'] = uid; + + await _db + .collection(doctorProfileCollectionName) + .doc(uid) + .set(doctorJson); + print('Doctor profile saved successfully'); + return true; + } catch (e) { + print('Error saving doctor profile: $e'); + return false; + } + } + + static Future getDoctorProfile() 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(doctorProfileCollectionName).doc(uid).get(); + + if (!doc.exists) { + print('No doctor profile found for this user'); + return null; + } + + final data = doc.data() as Map; + return Doctor( + profile: Profile.fromJson(data['profile']), + speciality: data['speciality'], + yearsOfExperience: data['yearsOfExperience'], + licenseNumber: data['licenseNumber'], + profileDescription: data['profileDescription'], + digitalSignature: data['digitalSignature'], + address: Address.fromJson(data['address']), + ); + } catch (e) { + print('Error fetching doctor profile: $e'); + return null; + } + } + + static Future updateDoctorProfile(DoctorController 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 Doctor doctorData = controller.model; + + final Map doctorJson = doctorData.toJson(); + doctorJson['updatedAt'] = FieldValue.serverTimestamp(); + + await _db + .collection(doctorProfileCollectionName) + .doc(uid) + .update(doctorJson); + print('Doctor profile updated successfully'); + return true; + } catch (e) { + print('Error updating doctor profile: $e'); + return false; + } + } + + static Future deleteDoctorProfile() async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return false; + } + + final String uid = user.uid; + + await _db.collection(doctorProfileCollectionName).doc(uid).delete(); + print('Doctor profile deleted successfully'); + return true; + } catch (e) { + print('Error deleting doctor profile: $e'); + return false; + } + } +} diff --git a/lib/data/services/navigation_service.dart b/lib/data/services/navigation_service.dart new file mode 100644 index 0000000..7531a73 --- /dev/null +++ b/lib/data/services/navigation_service.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/data/models/telemed_user.dart'; +import 'package:telemednet/data/services/data_service.dart'; +import 'package:telemednet/data/services/patient_registration_service.dart'; +import 'package:telemednet/route_names.dart'; + +class NavigationService { + static Future handleUserNavigation(BuildContext context) async { + try { + final TelemedUser? userProfile = await DataService.getProfile(); + + if (userProfile == null) { + if (context.mounted) { + Navigator.pushReplacementNamed(context, RouteNames.launch); + } + return; + } + + switch (userProfile.role.toLowerCase()) { + case 'doctor': + if (context.mounted) { + handleDoctorNavigation(context); + } + + break; + case 'patient': + if (context.mounted) { + handlePatientNavigation(context); + } + + break; + default: + if (context.mounted) { + Navigator.pushReplacementNamed(context, RouteNames.launch); + } + } + } catch (e) { + print('Error in handleUserNavigation: $e'); + if (context.mounted) { + Navigator.pushReplacementNamed(context, RouteNames.launch); + } + } + } + + static Future handleDoctorNavigation(BuildContext context) async { + if (context.mounted) { + Navigator.pushReplacementNamed( + context, + RouteNames.profileUpload, + ); + } + } + + static Future 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); + } + } + } +} diff --git a/lib/data/services/patient_registration_service.dart b/lib/data/services/patient_registration_service.dart new file mode 100644 index 0000000..a285b1a --- /dev/null +++ b/lib/data/services/patient_registration_service.dart @@ -0,0 +1,113 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:telemednet/data/models/patient.dart'; + +import '../../controller/patient_controller.dart'; + +class PatientProfileService { + static final String patientProfileCollectionName = + dotenv.env['PATIENT_PROFILE_COLLECTION_NAME']!; + static final FirebaseFirestore db = FirebaseFirestore.instance; + + static Future 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; + + final Map patientJson = patientData.toJson(); + patientJson['createdAt'] = FieldValue.serverTimestamp(); + patientJson['updatedAt'] = FieldValue.serverTimestamp(); + patientJson['uid'] = uid; + + 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 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; + return PatientModel.fromJson(data); + } catch (e) { + print('Error fetching patient profile: $e'); + return null; + } + } + + static Future updatePatientProfile(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; + + final Map patientJson = patientData.toJson(); + patientJson['updatedAt'] = FieldValue.serverTimestamp(); + + 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 deletePatientProfile() async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return false; + } + + final String uid = user.uid; + + await db.collection(patientProfileCollectionName).doc(uid).delete(); + + print('Patient profile deleted successfully'); + return true; + } catch (e) { + print('Error deleting patient profile: $e'); + return false; + } + } +} diff --git a/lib/route_names.dart b/lib/route_names.dart index 9d65d47..298bb68 100644 --- a/lib/route_names.dart +++ b/lib/route_names.dart @@ -4,6 +4,7 @@ class RouteNames { static const String userHome = '/user-home'; static const String signUp = '/sign-up'; static const String launch = '/launch'; + static const String profileUpload = '/profile-upload'; static const String patientLandingScreen = '/patient-landing-screen'; static const String patientDashboardScreen = '/patient-dahboard-screen'; static const String patientRegistrationScreen = @@ -12,4 +13,11 @@ class RouteNames { static const String patientFamilyMembersScreen = '/patient-family-members-screen'; static const String familyMembersEditScreen = '/family-members-edit-screen'; + static const String doctorAddressScreen = '/doctor-address-screen'; + static const String profileDescriptionScreen = '/doctor-profile-description'; + static const String experienceScreen = '/doctor-experience'; + static const String specialitiesScreeen = '/doctor-specialities'; + static const String digitalSignatureScreeen = '/doctor-signature'; + static const String achievementsScreen = '/doctor-achievements'; + static const String patientprofileScreen = '/patient-profile-screen'; } diff --git a/lib/routes.dart b/lib/routes.dart index 336c2e8..0c9acc5 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,18 +1,24 @@ -// routes.dart import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; -import 'package:telemednet/data/models/telemed_user.dart'; -import 'package:telemednet/screens/launch_screen.dart'; +import 'package:telemednet/screens/authentication/launch_screen.dart'; +import 'package:telemednet/screens/doctor_screens/Doctor_profile_screen.dart'; +import 'package:telemednet/controller/patient_controller.dart'; +import 'package:telemednet/screens/doctor_screens/achivements.dart'; +import 'package:telemednet/screens/doctor_screens/address_screen.dart'; import 'package:telemednet/route_names.dart'; -import 'package:telemednet/screens/patientDashboard/patient_dashboard_screen.dart'; -import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_adress_screen.dart'; -import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_family_members_screen.dart'; -import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_registration_screen.dart'; -import 'package:telemednet/screens/user_profile_screen.dart'; -import 'package:telemednet/screens/user_screen.dart'; +import 'package:telemednet/screens/doctor_screens/digital_signature.dart'; +import 'package:telemednet/screens/doctor_screens/experience_screen.dart'; +import 'package:telemednet/screens/doctor_screens/profile_description_screen.dart'; +import 'package:telemednet/screens/doctor_screens/specialities_selection.dart'; +import 'package:telemednet/screens/patientScreens/patientDashboard/patient_dashboard_screen.dart'; +import 'package:telemednet/screens/patientScreens/patientDashboard/patient_profile_screen.dart'; +import 'package:telemednet/screens/patientScreens/registrationScreens/patient_adress_screen.dart'; +import 'package:telemednet/screens/patientScreens/registrationScreens/patient_family_members_screen.dart'; +import 'package:telemednet/screens/patientScreens/registrationScreens/patient_registration_screen.dart'; -import 'screens/patientDashboard/patient_landing_screen.dart'; -import 'screens/patientDashboard/registrationScreens/family_members_edit_screen.dart'; +import 'controllers/doctor _controller.dart'; +import 'screens/patientScreens/patient_landing_screen.dart'; +import 'screens/patientScreens/registrationScreens/family_members_edit_screen.dart'; final Map routes = { RouteNames.launch: (context) => const LaunchScreen(), @@ -20,19 +26,53 @@ final Map routes = { providers: [EmailAuthProvider(), PhoneAuthProvider()], ), RouteNames.signUp: (context) => const RegisterScreen(), - RouteNames.userProfile: (context) { - var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?; - return UserProfileScreen(user: user); - }, - RouteNames.userHome: (context) => const UserScreen(), + // RouteNames.userProfile: (context) { + // var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?; + // return UserProfileScreen(user: user); + // }, + RouteNames.profileUpload: (context) => ProfileUploadPage(), RouteNames.patientLandingScreen: (context) => const PatientLandingScreen(), RouteNames.patientDashboardScreen: (context) => const PatientDashboardScreen(), RouteNames.patientRegistrationScreen: (context) => const PatientRegistrationScreen(), - RouteNames.patientAdressScreen: (context) => const PatientAddressScreen(), - RouteNames.patientFamilyMembersScreen: (context) => - const PatientFamilyMembersScreen(), - RouteNames.familyMembersEditScreen: (context) => - const FamilyMembersEditScreen(), + // RouteNames.patientAdressScreen: (context) => + // PatientAddressScreen(controller: PatientController()), + // RouteNames.patientFamilyMembersScreen: (context) => + // PatientFamilyMembersScreen(controller: PatientController()), + // RouteNames.familyMembersEditScreen: (context) => + // FamilyMembersEditScreen(controller: PatientController()), + RouteNames.doctorAddressScreen: (context) => DoctorAddressScreen( + controller: DoctorController(), // Provide the required controller + ), + RouteNames.profileDescriptionScreen: (context) => + ProfileDescriptionScreen(controller: DoctorController()), + RouteNames.experienceScreen: (context) => ExperienceScreen( + controller: DoctorController(), + ), + RouteNames.specialitiesScreeen: (context) => SpecialitiesScreen( + controller: DoctorController(), + ), + RouteNames.digitalSignatureScreeen: (context) => DigitalSignatureScreen( + controller: DoctorController(), + ), + RouteNames.achievementsScreen: (context) => + AchievementsScreen(controller: DoctorController()), + // FamilyMembersEditScreen(controller: PatientController()), + 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() }; diff --git a/lib/screens/authentication/launch_screen.dart b/lib/screens/authentication/launch_screen.dart index 75262d2..894c20f 100644 --- a/lib/screens/authentication/launch_screen.dart +++ b/lib/screens/authentication/launch_screen.dart @@ -1,9 +1,9 @@ import 'dart:async'; - import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -import 'package:telemednet/data/data_service.dart'; +import 'package:telemednet/data/services/navigation_service.dart'; import 'package:telemednet/route_names.dart'; +import 'package:telemednet/screens/authentication/sign_up_screen.dart'; import 'package:telemednet/widgets/primary_button.dart'; class LaunchScreen extends StatefulWidget { @@ -14,6 +14,8 @@ class LaunchScreen extends StatefulWidget { } class _LaunchScreenState extends State { + String? selectedUserType; + @override void initState() { super.initState(); @@ -22,102 +24,198 @@ class _LaunchScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Container( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('images/cover-picture.jpg'), - fit: BoxFit.cover, + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('images/cover-picture.jpg'), + fit: BoxFit.cover, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: StreamBuilder( + stream: FirebaseAuth.instance.authStateChanges(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildLoadingWidget(); + } else if (snapshot.hasData) { + _fetchProfileAndNavigate(context); + return _buildLoadingWidget(); + } else { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Register', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + const Text( + 'Who are you?', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + const SizedBox(height: 24), + _buildUserTypeSelection(context), + ], + ); + } + }, + ), ), ), - child: Container( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.all(18.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color.fromARGB(200, 255, 255, 255), - ), - child: SizedBox( - height: 150, - width: double.infinity, - child: Padding( - padding: const EdgeInsets.all(18.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'TelemedNet', - style: Theme.of(context).textTheme.titleLarge, - ), - StreamBuilder( - stream: - FirebaseAuth.instance.authStateChanges(), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasData) { - _fetchProfileAndNavigate(context); - return _buildProceedingWidget(context); - } else { - return _buildSignInSignUpRow(context); - } - }) - ], - )), - ), - )), - ), - ))); + ), + ), + ), + ); } - Future _fetchProfileAndNavigate(BuildContext context) async { - var profile = await DataService.getProfile(); - if (mounted) { - setState(() { - if (profile == null) { - Navigator.of(context) - .pushReplacementNamed(RouteNames.userProfile, arguments: profile); - } else { - Navigator.of(context) - .pushReplacementNamed(RouteNames.userHome, arguments: profile); - } - }); - } + Widget _buildLoadingWidget() { + return const SizedBox( + height: 120, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text('Please wait...'), + ], + ), + ), + ); } - Widget _buildProceedingWidget(BuildContext context) { - return const Column( + Widget _buildUserTypeSelection(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - CircularProgressIndicator(), - SizedBox(height: 10), - Text('Please wait...') + _buildSelectionCard( + title: 'Doctor', + description: 'Can organise and approve appointments', + icon: Icons.medical_services, + isSelected: selectedUserType == 'doctor', + onTap: () => setState(() => selectedUserType = 'doctor'), + ), + const SizedBox(height: 12), + _buildSelectionCard( + title: 'Patient', + description: 'Can book appointments', + icon: Icons.person, + isSelected: selectedUserType == 'patient', + onTap: () => setState(() => selectedUserType = 'patient'), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: PrimaryButton( + onPressed: selectedUserType != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => SignUpScreen( + selectedUserType: selectedUserType!, + ), + ), + ) + : null, + text: 'Next', + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Already have an account?'), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed(RouteNames.signIn); + }, + child: const Text( + 'Login', + style: TextStyle(color: Colors.blue), + ), + ), + ], + ), ], ); } - Widget _buildSignInSignUpRow(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pushNamed(RouteNames.signUp); - }, - child: const Text('Sign Up'))), - const SizedBox(width: 10), - Expanded( - child: PrimaryButton( - onPressed: () { - Navigator.of(context).pushNamed(RouteNames.signIn); - }, - text: 'Sign In', - )), - ]); + Widget _buildSelectionCard({ + required String title, + required String description, + required IconData icon, + required bool isSelected, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + borderRadius: BorderRadius.circular(12), + color: isSelected ? Colors.blue.shade50 : Colors.white, + ), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: Colors.blue, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _fetchProfileAndNavigate(BuildContext context) async { + if (mounted) { + await NavigationService.handleUserNavigation(context); + } } } diff --git a/lib/screens/authentication/sign_up_screen.dart b/lib/screens/authentication/sign_up_screen.dart new file mode 100644 index 0000000..e60a34f --- /dev/null +++ b/lib/screens/authentication/sign_up_screen.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:intl_phone_field/intl_phone_field.dart'; +import 'package:telemednet/data/services/data_service.dart'; +import 'package:telemednet/data/services/navigation_service.dart'; +import 'package:telemednet/widgets/primary_button.dart'; + +class SignUpScreen extends StatefulWidget { + final String selectedUserType; + + const SignUpScreen({ + super.key, + required this.selectedUserType, + }); + + @override + State createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + String _completePhoneNumber = ''; + bool _isLoading = false; + bool _obscurePassword = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Sign Up'), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration(), + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Register as ${widget.selectedUserType}', + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Column( + children: [ + TextFormField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + prefixIcon: const Icon(Icons.email_outlined, + color: Colors.blue), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter your email'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') + .hasMatch(value!)) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.blue), + ), + prefixIcon: + const Icon(Icons.lock_outline, color: Colors.blue), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.blue, + ), + onPressed: () => setState( + () => _obscurePassword = !_obscurePassword), + ), + ), + obscureText: _obscurePassword, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter your password'; + } + if ((value?.length ?? 0) < 6) { + return 'Password must be at least 6 characters'; + } + return null; + }, + ), + const SizedBox(height: 16), + IntlPhoneField( + decoration: InputDecoration( + labelText: 'Phone Number', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.blue), + ), + ), + initialCountryCode: 'IN', + onChanged: (phone) { + _completePhoneNumber = phone.completeNumber; + }, + validator: (phone) { + if (phone?.completeNumber.isEmpty ?? true) { + return 'Please enter your phone number'; + } + return null; + }, + dropdownTextStyle: const TextStyle(color: Colors.blue), + style: const TextStyle(color: Colors.blue), + ), + ], + ), + const SizedBox(height: 24), + PrimaryButton( + onPressed: _isLoading ? null : _handleSignUp, + text: _isLoading ? 'Creating Account...' : 'Create Account', + icon: Icons.person_add, + ), + ], + ), + ), + ), + ), + ); + } + + Future _handleSignUp() async { + if (_formKey.currentState == null || !_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final result = await DataService.createUserProfile( + email: _emailController.text.trim(), + password: _passwordController.text, + userType: widget.selectedUserType, + 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']); + } + } + } catch (e) { + if (mounted) { + _showErrorSnackBar('An unexpected error occurred. Please try again.'); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/authentication/user_profile_screen.dart b/lib/screens/authentication/user_profile_screen.dart deleted file mode 100644 index f6a09d3..0000000 --- a/lib/screens/authentication/user_profile_screen.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:telemednet/data/models/telemed_user.dart'; -import 'package:telemednet/route_names.dart'; -import 'package:telemednet/shared/user_selection.dart'; - -class UserProfileScreen extends StatefulWidget { - final TelemedUser? user; - const UserProfileScreen({super.key, required this.user}); - - @override - State createState() => _UserProfileScreenState(); -} - -class _UserProfileScreenState extends State { - TelemedUser? user; - final FirebaseAuth _auth = FirebaseAuth.instance; - - @override - void initState() { - super.initState(); - user = widget.user; - } - - Future _signOut() async { - try { - await _auth.signOut(); - // Navigate to login screen or home screen after logout - Navigator.of(context).pushReplacementNamed(RouteNames.launch); - } catch (e) { - print("Error signing out: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to log out. Please try again.')), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('User Profile'), - actions: [ - IconButton( - icon: Icon(Icons.exit_to_app), - onPressed: _signOut, - ), - ], - ), - body: Column( - children: [ - Expanded(child: UserSelection()), - ElevatedButton( - onPressed: _signOut, - child: Text('Log Out'), - ), - ], - ), - ); - } -} diff --git a/lib/screens/doctor_screens/Doctor_profile_screen.dart b/lib/screens/doctor_screens/Doctor_profile_screen.dart new file mode 100644 index 0000000..ba044d7 --- /dev/null +++ b/lib/screens/doctor_screens/Doctor_profile_screen.dart @@ -0,0 +1,200 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:telemednet/route_names.dart'; +import 'package:telemednet/screens/doctor_screens/address_screen.dart'; +import '../../controllers/doctor _controller.dart'; + +class ProfileUploadPage extends StatefulWidget { + @override + _ProfileUploadPageState createState() => _ProfileUploadPageState(); +} + +class _ProfileUploadPageState extends State { + final DoctorController _controller = DoctorController(); + final _titleController = TextEditingController(); + final _surnameController = TextEditingController(); + final _firstnameController = TextEditingController(); + final _middlenameController = TextEditingController(); + final _qualificationController = TextEditingController(); + File? _image; + + @override + void dispose() { + _titleController.dispose(); + _surnameController.dispose(); + _firstnameController.dispose(); + _middlenameController.dispose(); + _qualificationController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + }); + } + } + + void _addQualification() { + if (_qualificationController.text.isNotEmpty) { + setState(() { + _controller.profileController + .addQualification(_qualificationController.text); + _qualificationController.clear(); + }); + } + } + + void _removeQualification(String qualification) { + setState(() { + _controller.profileController.removeQualification(qualification); + }); + } + + bool _validateAndProceed() { + _controller.profileController.updateTitle(_titleController.text); + _controller.profileController.updateSurName(_surnameController.text); + _controller.profileController.updateLastName(_firstnameController.text); + _controller.profileController.updateMiddleName(_middlenameController.text); + + return _controller.profileController.validateProfile(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Doctor Profile'), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: GestureDetector( + onTap: _pickImage, + child: CircleAvatar( + radius: 60, + backgroundImage: _image != null ? FileImage(_image!) : null, + child: + _image == null ? Icon(Icons.camera_alt, size: 50) : null, + ), + ), + ), + SizedBox(height: 24), + _buildTextField( + 'Title', + 'Mr, Ms..', + _titleController, + (value) => _controller.profileController.updateTitle(value), + ), + _buildTextField( + 'Surname', + 'Enter surname', + _surnameController, + (value) => _controller.profileController.updateSurName(value), + ), + _buildTextField( + 'Firstname', + 'Enter firstname', + _firstnameController, + (value) => _controller.profileController.updateLastName(value), + ), + _buildTextField( + 'Middle name', + 'Enter middle name', + _middlenameController, + (value) => _controller.profileController.updateMiddleName(value), + ), + SizedBox(height: 16), + Text( + 'Qualifications', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _qualificationController, + decoration: InputDecoration( + hintText: 'Add Qualification', + border: OutlineInputBorder(), + ), + ), + ), + SizedBox(width: 8), + ElevatedButton( + onPressed: _addQualification, + child: Icon(Icons.add), + style: ElevatedButton.styleFrom( + shape: CircleBorder(), + padding: EdgeInsets.all(12), + ), + ), + ], + ), + SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: _controller.profileController.model.qualifications + .map((qual) => Chip( + label: Text(qual), + deleteIcon: Icon(Icons.close), + onDeleted: () => _removeQualification(qual), + )) + .toList(), + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed( + context, + RouteNames.doctorAddressScreen, + arguments: DoctorAddressScreen(controller: _controller), + ); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + backgroundColor: const Color(0xFF5BC0DE)), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ), + ); + } + + Widget _buildTextField( + String label, + String hint, + TextEditingController controller, + Function(String) onChanged, + ) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: OutlineInputBorder(), + ), + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/screens/doctor_screens/achivements.dart b/lib/screens/doctor_screens/achivements.dart new file mode 100644 index 0000000..eee0f2c --- /dev/null +++ b/lib/screens/doctor_screens/achivements.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../route_names.dart'; + +class AchievementsScreen extends StatefulWidget { + final DoctorController controller; + + const AchievementsScreen({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _AchievementsScreenState(); +} + +class _AchievementsScreenState extends State { + final TextEditingController _achievementController = TextEditingController(); + late final DoctorController _controller; + late List achievements; + bool _isEditing = false; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + _achievementController.addListener(_onFieldChanged); + // Create a new modifiable list from the controller's achievements + achievements = List.from(_controller.model.achievements); + } + + void _addAchievement() { + final achievement = _achievementController.text.trim(); + if (achievement.isNotEmpty) { + setState(() { + achievements.add(achievement); + _isEditing = true; + }); + _controller.updateAchievements(List.from(achievements)); + _achievementController.clear(); + } else { + _showError('Please enter an achievement'); + } + } + + void _removeAchievement(int index) { + setState(() { + achievements.removeAt(index); + _isEditing = true; + }); + _controller.updateAchievements(List.from(achievements)); + } + + @override + void dispose() { + _achievementController.removeListener(_onFieldChanged); + _achievementController.dispose(); + super.dispose(); + } + + void _onFieldChanged() { + setState(() { + _isEditing = true; + }); + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + bool _validateAchievements() { + if (achievements.isEmpty) { + _showError('Please add at least one achievement'); + return false; + } + return true; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_isEditing) { + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('DISCARD'), + ), + ], + ), + ); + return shouldPop ?? false; + } + return true; + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Achievements'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (_isEditing) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('DISCARD'), + ), + ], + ), + ); + } else { + Navigator.pop(context); + } + }, + ), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Add Your Achievements', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + 'List your professional accomplishments, certifications, and awards', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + controller: _achievementController, + decoration: InputDecoration( + hintText: 'Enter your achievement', + suffixIcon: IconButton( + icon: const Icon(Icons.add), + onPressed: _addAchievement, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onSubmitted: (_) => _addAchievement(), + ), + ), + Expanded( + child: ListView.builder( + itemCount: achievements.length, + padding: const EdgeInsets.all(16), + itemBuilder: (context, index) { + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: const Color(0xFF5BC0DE), + child: Text('${index + 1}'), + ), + title: Text(achievements[index]), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeAchievement(index), + ), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Draft saved')), + ); + }, + child: const Text('SAVE DRAFT'), + ), + ElevatedButton( + onPressed: () { + if (_validateAchievements()) { + Navigator.pushNamed( + context, + RouteNames.digitalSignatureScreeen, + arguments: _controller, + ); + } + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + backgroundColor: const Color(0xFF5BC0DE), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/doctor_screens/address_screen.dart b/lib/screens/doctor_screens/address_screen.dart new file mode 100644 index 0000000..77f838e --- /dev/null +++ b/lib/screens/doctor_screens/address_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/screens/doctor_screens/profile_description_screen.dart'; +import '../../common/custom_style.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../route_names.dart'; + +class DoctorAddressScreen extends StatefulWidget { + final DoctorController controller; + + const DoctorAddressScreen({Key? key, required this.controller}) + : super(key: key); + + @override + State createState() => _DoctorAddressScreenState(); +} + +class _DoctorAddressScreenState extends State { + final _floorBuildingController = TextEditingController(); + final _streetController = TextEditingController(); + final _cityController = TextEditingController(); + final _stateController = TextEditingController(); + final _countryController = TextEditingController(); + final _postalCodeController = TextEditingController(); + + late AddressController addressController; + + @override + void initState() { + super.initState(); + + // Initialize the AddressController + addressController = widget.controller.addressController; + + // Set the initial values from the address model + _floorBuildingController.text = addressController.model.floorBuilding ?? ''; + _streetController.text = addressController.model.street ?? ''; + _cityController.text = addressController.model.city ?? ''; + _stateController.text = addressController.model.state ?? ''; + _countryController.text = addressController.model.country ?? ''; + _postalCodeController.text = addressController.model.postalCode ?? ''; + } + + @override + void dispose() { + _floorBuildingController.dispose(); + _streetController.dispose(); + _cityController.dispose(); + _stateController.dispose(); + _countryController.dispose(); + _postalCodeController.dispose(); + super.dispose(); + } + + bool _validateAndProceed() { + // Update the address model + addressController.updateFloorBuilding(_floorBuildingController.text); + addressController.updateStreet(_streetController.text); + addressController.updateCity(_cityController.text); + addressController.updateState(_stateController.text); + addressController.updateCountry(_countryController.text); + addressController.updatePostalCode(_postalCodeController.text); + + // Validate the address fields + if (_areFieldsValid()) { + return true; + } + + showErrorSnackBar(context, 'Please fill in all required fields'); + return false; + } + + bool _areFieldsValid() { + return _floorBuildingController.text.isNotEmpty && + _streetController.text.isNotEmpty && + _cityController.text.isNotEmpty && + _stateController.text.isNotEmpty && + _countryController.text.isNotEmpty && + _postalCodeController.text.isNotEmpty; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Address Details'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTextField( + 'Floor, Building', + 'Enter floor and building', + _floorBuildingController, + ), + _buildTextField( + 'Street or Road', + 'Enter street or road', + _streetController, + ), + _buildTextField( + 'City', + 'Enter city', + _cityController, + ), + _buildTextField( + 'State', + 'Enter state', + _stateController, + ), + _buildTextField( + 'Country', + 'Enter country', + _countryController, + ), + _buildTextField( + 'Postal Code', + 'Enter postal code', + _postalCodeController, + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed( + context, + RouteNames.profileDescriptionScreen, + arguments: ProfileDescriptionScreen( + controller: + widget.controller, // Pass the same controller instance + ), + ); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + backgroundColor: const Color(0xFF5BC0DE), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ), + ); + } + + Widget _buildTextField( + String label, + String hint, + TextEditingController controller, + ) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: const OutlineInputBorder(), + ), + ), + ); + } +} diff --git a/lib/screens/doctor_screens/digital_signature.dart b/lib/screens/doctor_screens/digital_signature.dart new file mode 100644 index 0000000..8b53805 --- /dev/null +++ b/lib/screens/doctor_screens/digital_signature.dart @@ -0,0 +1,424 @@ +// // import 'package:flutter/material.dart'; +// // import 'dart:io'; +// // import 'package:image_picker/image_picker.dart'; +// // import '../../controllers/doctor _controller.dart'; + +// // class DigitalSignatureScreen extends StatefulWidget { +// // final DoctorController controller; + +// // const DigitalSignatureScreen({ +// // Key? key, +// // required this.controller, +// // }) : super(key: key); + +// // @override +// // State createState() => _DigitalSignatureScreenState(); +// // } + +// // class _DigitalSignatureScreenState extends State { +// // File? _signatureFile; +// // final ImagePicker _picker = ImagePicker(); + +// // Future _pickImage() async { +// // try { +// // final XFile? image = await _picker.pickImage(source: ImageSource.gallery); +// // if (image != null) { +// // setState(() { +// // _signatureFile = File(image.path); +// // }); +// // widget.controller.updateDigitalSignature(image.path); +// // } +// // } catch (e) { +// // debugPrint('Error picking image: $e'); +// // } +// // } + +// // @override +// // Widget build(BuildContext context) { +// // return Scaffold( +// // appBar: AppBar( +// // title: const Text( +// // 'Digital Signature', +// // style: TextStyle( +// // fontSize: 24, +// // fontWeight: FontWeight.bold, +// // ), +// // ), +// // actions: [ +// // TextButton( +// // onPressed: () { +// // Navigator.pop(context); +// // }, +// // child: const Text( +// // 'Skip', +// // style: TextStyle( +// // color: Color(0xFF5BC0DE), +// // fontSize: 16, +// // ), +// // ), +// // ), +// // ], +// // ), +// // body: Column( +// // crossAxisAlignment: CrossAxisAlignment.center, +// // children: [ +// // const SizedBox(height: 20), +// // Center( +// // child: SizedBox( +// // width: 605, +// // height: 205, +// // child: Container( +// // decoration: BoxDecoration( +// // border: Border.all(color: Colors.grey.shade300), +// // borderRadius: BorderRadius.circular(12), +// // ), +// // child: Stack( +// // children: [ +// // if (_signatureFile != null) +// // ClipRRect( +// // borderRadius: BorderRadius.circular(12), +// // child: Image.file( +// // _signatureFile!, +// // width: 605, +// // height: 205, +// // fit: BoxFit.contain, +// // ), +// // ), +// // Positioned( +// // right: 16, +// // bottom: 16, +// // child: ElevatedButton.icon( +// // onPressed: _pickImage, +// // icon: const Icon(Icons.upload), +// // label: const Text('Upload'), +// // style: ElevatedButton.styleFrom( +// // backgroundColor: Colors.white, +// // foregroundColor: Colors.black, +// // shape: RoundedRectangleBorder( +// // borderRadius: BorderRadius.circular(20), +// // ), +// // ), +// // ), +// // ), +// // ], +// // ), +// // ), +// // ), +// // ), +// // const Spacer(), +// // Padding( +// // padding: const EdgeInsets.all(16), +// // child: ElevatedButton( +// // onPressed: () { +// // if (widget.controller.validateProfile()) { +// // Navigator.pop(context); +// // } +// // }, +// // style: ElevatedButton.styleFrom( +// // backgroundColor: const Color(0xFF5BC0DE), +// // shape: const CircleBorder(), +// // padding: const EdgeInsets.all(24), +// // ), +// // child: const Icon( +// // Icons.arrow_forward_ios, +// // color: Colors.white, +// // ), +// // ), +// // ), +// // ], +// // ), +// // ); +// // } +// // } +// import 'package:flutter/material.dart'; +// import 'dart:io'; +// import 'package:image_picker/image_picker.dart'; +// import '../../controllers/doctor _controller.dart'; + +// class DigitalSignatureScreen extends StatefulWidget { +// final DoctorController controller; + +// const DigitalSignatureScreen({ +// Key? key, +// required this.controller, +// }) : super(key: key); + +// @override +// State createState() => _DigitalSignatureScreenState(); +// } + +// class _DigitalSignatureScreenState extends State { +// File? _signatureFile; +// final ImagePicker _picker = ImagePicker(); + +// Future _pickImage() async { +// try { +// final XFile? image = await _picker.pickImage(source: ImageSource.gallery); +// if (image != null) { +// setState(() { +// _signatureFile = File(image.path); +// }); +// widget.controller.updateDigitalSignature(image.path); +// } +// } catch (e) { +// debugPrint('Error picking image: $e'); +// } +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text( +// 'Digital Signature', +// style: TextStyle( +// fontSize: 24, +// fontWeight: FontWeight.bold, +// ), +// ), +// actions: [ +// TextButton( +// onPressed: () { +// Navigator.pop(context); +// }, +// child: const Text( +// 'Save and Skip', +// style: TextStyle( +// color: Color(0xFF5BC0DE), +// fontSize: 16, +// ), +// ), +// ), +// ], +// ), +// body: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// const SizedBox(height: 20), +// Center( +// child: SizedBox( +// width: 605, +// height: 205, +// child: Container( +// decoration: BoxDecoration( +// border: Border.all(color: Colors.grey.shade300), +// borderRadius: BorderRadius.circular(12), +// ), +// child: Stack( +// children: [ +// if (_signatureFile != null) +// ClipRRect( +// borderRadius: BorderRadius.circular(12), +// child: Image.file( +// _signatureFile!, +// width: 605, +// height: 205, +// fit: BoxFit.contain, +// ), +// ), +// if (_signatureFile == null) +// Center( +// child: Text( +// 'No signature uploaded', +// style: TextStyle( +// color: Colors.grey.shade400, +// fontSize: 16, +// ), +// ), +// ), +// ], +// ), +// ), +// ), +// ), +// const SizedBox(height: 20), +// ElevatedButton.icon( +// onPressed: _pickImage, +// icon: const Icon(Icons.upload), +// label: const Text('Upload Signature'), +// style: ElevatedButton.styleFrom( +// backgroundColor: const Color(0xFF5BC0DE), +// foregroundColor: Colors.white, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(8), +// ), +// ), +// ), +// const Spacer(), +// Padding( +// padding: const EdgeInsets.all(16), +// child: ElevatedButton( +// onPressed: () { +// if (widget.controller.validateProfile()) { +// Navigator.pop(context); +// } +// }, +// style: ElevatedButton.styleFrom( +// backgroundColor: const Color(0xFF5BC0DE), +// shape: const CircleBorder(), +// padding: const EdgeInsets.all(24), +// ), +// child: const Icon( +// Icons.arrow_forward_ios, +// color: Colors.white, +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } +import 'package:flutter/material.dart'; +import 'dart:io'; +import 'package:image_picker/image_picker.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../data/services/doctor_profile_service.dart'; +import '../../route_names.dart'; + +class DigitalSignatureScreen extends StatefulWidget { + final DoctorController controller; + + const DigitalSignatureScreen({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _DigitalSignatureScreenState(); +} + +class _DigitalSignatureScreenState extends State { + File? _signatureFile; + final ImagePicker _picker = ImagePicker(); + + Future _pickImage() async { + try { + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _signatureFile = File(image.path); + }); + widget.controller.updateDigitalSignature(image.path); + } + } catch (e) { + debugPrint('Error picking image: $e'); + } + } + + Future _saveProfile() async { + // Directly save the doctor profile without validation + bool success = + await DoctorProfileService.saveDoctorProfile(widget.controller); + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Doctor profile saved successfully!')), + ); + Navigator.of(context).pushNamed(RouteNames.doctorAddressScreen); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to save profile. Please try again.')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Digital Signature', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + TextButton( + onPressed: _saveProfile, + child: const Text( + 'Save and Skip', + style: TextStyle( + color: Color(0xFF5BC0DE), + fontSize: 16, + ), + ), + ), + ], + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + Center( + child: SizedBox( + width: 605, + height: 205, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Stack( + children: [ + if (_signatureFile != null) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + _signatureFile!, + width: 605, + height: 205, + fit: BoxFit.contain, + ), + ), + if (_signatureFile == null) + Center( + child: Text( + 'No signature uploaded', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: _pickImage, + icon: const Icon(Icons.upload), + label: const Text('Upload Signature'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF5BC0DE), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: _saveProfile, // Call _saveProfile here + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF5BC0DE), + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/doctor_screens/experience_screen.dart b/lib/screens/doctor_screens/experience_screen.dart new file mode 100644 index 0000000..caed6d4 --- /dev/null +++ b/lib/screens/doctor_screens/experience_screen.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../route_names.dart'; + +class ExperienceScreen extends StatefulWidget { + final DoctorController controller; + + const ExperienceScreen({Key? key, required this.controller}) + : super(key: key); + + @override + _ExperienceScreenState createState() => _ExperienceScreenState(); +} + +class _ExperienceScreenState extends State { + String? _selectedExperience; + final _licenseController = TextEditingController(); + late final DoctorController _controller; + bool _isEditing = false; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + _selectedExperience = _controller.model.yearsOfExperience; + _licenseController.text = _controller.model.licenseNumber ?? ''; + _licenseController.addListener(_onFieldChanged); + } + + @override + void dispose() { + _licenseController.removeListener(_onFieldChanged); + _licenseController.dispose(); + super.dispose(); + } + + void _onFieldChanged() { + setState(() { + _isEditing = true; + }); + } + + bool _validateFields() { + if (_selectedExperience == null) { + _showError('Please select years of experience'); + return false; + } + + if (_licenseController.text.trim().isEmpty) { + _showError('Please enter your license number'); + return false; + } + + // Add additional license number validation if needed + if (_licenseController.text.trim().length < 5) { + _showError('License number must be at least 5 characters long'); + return false; + } + + return true; + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + bool _validateAndProceed() { + if (!_validateFields()) { + return false; + } + + _controller.updateYearsOfExperience(_selectedExperience!); + _controller.updateLicenseNumber(_licenseController.text.trim()); + return true; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_isEditing) { + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('DISCARD'), + ), + ], + ), + ); + return shouldPop ?? false; + } + return true; + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Experience Details'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (_isEditing) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('DISCARD'), + ), + ], + ), + ); + } else { + Navigator.pop(context); + } + }, + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Professional Experience', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Please provide your years of experience and medical license details.', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + const SizedBox(height: 24), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Years of Experience', + border: OutlineInputBorder(), + ), + value: _selectedExperience, + items: List.generate(50, (index) => index + 1) + .map((year) => DropdownMenuItem( + value: year.toString(), + child: Text('$year years'), + )) + .toList(), + onChanged: (value) { + setState(() { + _selectedExperience = value; + _isEditing = true; + }); + }, + ), + const SizedBox(height: 16), + TextField( + controller: _licenseController, + decoration: const InputDecoration( + labelText: 'License Number', + hintText: 'Enter your medical license number', + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Draft saved')), + ); + }, + child: const Text('SAVE DRAFT'), + ), + ElevatedButton( + onPressed: () { + if (_validateAndProceed()) { + Navigator.pushNamed( + context, + RouteNames.specialitiesScreeen, + arguments: _controller, + ); + } + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + backgroundColor: const Color(0xFF5BC0DE), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/doctor_screens/profile_description_screen.dart b/lib/screens/doctor_screens/profile_description_screen.dart new file mode 100644 index 0000000..e624316 --- /dev/null +++ b/lib/screens/doctor_screens/profile_description_screen.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../route_names.dart'; + +class ProfileDescriptionScreen extends StatefulWidget { + final DoctorController controller; + + const ProfileDescriptionScreen({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + _ProfileDescriptionScreenState createState() => + _ProfileDescriptionScreenState(); +} + +class _ProfileDescriptionScreenState extends State { + final _descriptionController = TextEditingController(); + late final DoctorController _controller; + bool _isEditing = false; + final int _minDescriptionLength = 50; // Minimum description length + final int _maxDescriptionLength = 500; // Maximum description length + + @override + void initState() { + super.initState(); + _controller = widget.controller; + // Initialize with existing description if any + _descriptionController.text = + _controller.profileController.model.profileDescription ?? ''; + // Add listener to track editing state + _descriptionController.addListener(_onDescriptionChanged); + } + + @override + void dispose() { + _descriptionController.removeListener(_onDescriptionChanged); + _descriptionController.dispose(); + super.dispose(); + } + + void _onDescriptionChanged() { + setState(() { + _isEditing = true; + }); + } + + bool _validateDescription() { + final description = _descriptionController.text.trim(); + + if (description.isEmpty) { + _showError('Please enter a profile description'); + return false; + } + + if (description.length < _minDescriptionLength) { + _showError( + 'Description must be at least $_minDescriptionLength characters long'); + return false; + } + + if (description.length > _maxDescriptionLength) { + _showError('Description cannot exceed $_maxDescriptionLength characters'); + return false; + } + + return true; + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + bool _validateAndProceed() { + if (!_validateDescription()) { + return false; + } + + // Update the profile description in the controller + _controller.updateProfileDescription(_descriptionController.text.trim()); + return true; + } + + String _getCharacterCount() { + return '${_descriptionController.text.length}/$_maxDescriptionLength'; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_isEditing) { + // Show confirmation dialog if there are unsaved changes + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('DISCARD'), + ), + ], + ), + ); + return shouldPop ?? false; + } + return true; + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Profile Description'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (_isEditing) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('DISCARD'), + ), + ], + ), + ); + } else { + Navigator.pop(context); + } + }, + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tell us about yourself', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Write a brief description about your professional background, expertise, and approach to patient care.', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _descriptionController, + maxLines: 8, + maxLength: _maxDescriptionLength, + decoration: InputDecoration( + labelText: 'Profile Description', + hintText: + 'Enter your professional background and expertise...', + border: const OutlineInputBorder(), + alignLabelWithHint: true, + counterText: _getCharacterCount(), + ), + textInputAction: TextInputAction.newline, + // keyboardType: TextInputAction.multiline, + ), + const SizedBox(height: 8), + Text( + 'Minimum $_minDescriptionLength characters required', + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + // Save draft functionality can be added here + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Draft saved')), + ); + }, + child: const Text('SAVE DRAFT'), + ), + ElevatedButton( + onPressed: () { + { + Navigator.pushNamed( + context, + RouteNames.experienceScreen, + arguments: _controller, + ); + } + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + backgroundColor: const Color(0xFF5BC0DE), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/doctor_screens/specialities_selection.dart b/lib/screens/doctor_screens/specialities_selection.dart new file mode 100644 index 0000000..396a556 --- /dev/null +++ b/lib/screens/doctor_screens/specialities_selection.dart @@ -0,0 +1,296 @@ +import 'package:flutter/material.dart'; +import '../../controllers/doctor _controller.dart'; +import '../../route_names.dart'; + +class SpecialitiesScreen extends StatefulWidget { + final DoctorController controller; + + const SpecialitiesScreen({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _SpecialitiesScreenState(); +} + +class _SpecialitiesScreenState extends State { + String? selectedSpeciality; + late final DoctorController _controller; + bool _isEditing = false; + + final List> specialities = [ + { + 'icon': Icons.child_care, + 'label': 'Pediatric', + 'value': 'pediatric', + 'description': 'Specialist in child healthcare', + }, + { + 'icon': Icons.medical_services, + 'label': 'Casual', + 'value': 'casual', + 'description': 'General healthcare provider', + }, + { + 'icon': Icons.coronavirus, + 'label': 'Corona', + 'value': 'corona', + 'description': 'COVID-19 specialist', + }, + { + 'icon': Icons.pregnant_woman, + 'label': 'Gynecology', + 'value': 'gynecology', + 'description': 'Women\'s health specialist', + }, + { + 'icon': Icons.medical_services_outlined, + 'label': 'Orthopedic', + 'value': 'orthopedic', + 'description': 'Bone and joint specialist', + }, + { + 'icon': Icons.remove_red_eye, + 'label': 'Eye', + 'value': 'eye', + 'description': 'Eye care specialist', + }, + { + 'icon': Icons.psychology, + 'label': 'Psychiatrist', + 'value': 'psychiatrist', + 'description': 'Mental health specialist', + }, + { + 'icon': Icons.medical_information, + 'label': 'Dental', + 'value': 'dental', + 'description': 'Dental care specialist', + }, + { + 'icon': Icons.person, + 'label': 'General', + 'value': 'general', + 'description': 'General practitioner', + }, + ]; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + selectedSpeciality = _controller.model.speciality; + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + bool _validateSelection() { + if (selectedSpeciality == null) { + _showError('Please select a speciality to continue'); + return false; + } + return true; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_isEditing) { + final shouldPop = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('DISCARD'), + ), + ], + ), + ); + return shouldPop ?? false; + } + return true; + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (_isEditing) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Discard Changes?'), + content: const Text( + 'You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text('DISCARD'), + ), + ], + ), + ); + } else { + Navigator.pop(context); + } + }, + ), + title: const Text('Choose Speciality'), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Select Your Specialization', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + 'Choose the medical field that best represents your expertise', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ], + ), + ), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: specialities.length, + itemBuilder: (context, index) { + final specialty = specialities[index]; + final isSelected = selectedSpeciality == specialty['value']; + + return GestureDetector( + onTap: () { + setState(() { + selectedSpeciality = specialty['value']; + _isEditing = true; + }); + _controller.updateSpeciality(specialty['value']); + }, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF5BC0DE) + : Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all( + color: Colors.white, + width: 2, + ) + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + specialty['icon'], + size: 32, + color: isSelected ? Colors.white : Colors.grey, + ), + const SizedBox(height: 8), + Text( + specialty['label'], + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + if (isSelected) + const Icon( + Icons.check_circle, + color: Colors.white, + size: 20, + ), + ], + ), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Draft saved')), + ); + }, + child: const Text('SAVE DRAFT'), + ), + ElevatedButton( + onPressed: () { + if (_validateSelection()) { + Navigator.pushNamed( + context, + RouteNames.achievementsScreen, + arguments: _controller, + ); + } + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(24), + backgroundColor: const Color(0xFF5BC0DE), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/patientDashboard/patient_landing_screen.dart b/lib/screens/patientDashboard/patient_landing_screen.dart index 4fffa26..e6f8b7d 100644 --- a/lib/screens/patientDashboard/patient_landing_screen.dart +++ b/lib/screens/patientDashboard/patient_landing_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:telemednet/route_names.dart'; class PatientLandingScreen extends StatelessWidget { - const PatientLandingScreen({Key? key}) : super(key: key); + const PatientLandingScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/screens/patientScreens/patientDashboard/patient_dashboard_screen.dart b/lib/screens/patientScreens/patientDashboard/patient_dashboard_screen.dart new file mode 100644 index 0000000..66b9859 --- /dev/null +++ b/lib/screens/patientScreens/patientDashboard/patient_dashboard_screen.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/route_names.dart'; + +class PatientDashboardScreen extends StatefulWidget { + const PatientDashboardScreen({super.key}); + + @override + State createState() => _PatientDashboardScreenState(); +} + +class _PatientDashboardScreenState extends State { + @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(), + ], + ), + ), + _buildBottomNavBar(), + ], + ), + ), + ); + } + + Widget _buildSearchBar() { + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Color.fromRGBO(96, 181, 250, 1), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(50.0), + bottomRight: Radius.circular(50.0)), + ), + child: TextField( + decoration: InputDecoration( + hintText: 'Search Doctor/Hospital/Symtoms', + prefixIcon: const Icon(Icons.search), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ), + ); + } + + Widget _buildRealTimeCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.lightBlue[100]!, Colors.lightBlue[50]!], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Real-time care\nat your fingertips.', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: const Text('Consultation >'), + ), + ], + ), + ); + } + + Widget _buildConsultationsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Consultations', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _consultationCard('Dr Pom', '23/09/2024\n5:00AM-7:00AM'), + const SizedBox(width: 10), + _consultationCard('Dr I', '23/09/2024\n5:00AM-7:00AM'), + ], + ), + ), + ], + ); + } + + Widget _consultationCard(String name, String schedule) { + return GestureDetector( + onTap: () { + print('Tapped on consultation card for $name'); + }, + child: Card( + shadowColor: Colors.grey, + child: Container( + width: 200, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow(color: Colors.grey.withOpacity(0.5), blurRadius: 5), + ], + ), + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Colors.blue[100], + child: const Icon(Icons.person, size: 40, color: Colors.white), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, + style: const TextStyle(fontWeight: FontWeight.bold)), + Text(schedule, style: const TextStyle(fontSize: 12)), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildFindDoctorSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Find a Doctor for your\nHealth Problem', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _categoryIcon(Icons.accessibility_new, Colors.blue), + _categoryIcon(Icons.remove_red_eye, Colors.blue), + _categoryIcon(Icons.medical_services, Colors.blue), + _categoryIcon(Icons.health_and_safety, Colors.blue), + _categoryIcon(Icons.child_care, Colors.blue), + ], + ), + ], + ); + } + + Widget _categoryIcon(IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: Colors.white, size: 30), + ); + } + + Widget _buildBottomNavBar() { + return BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.chat_bubble), label: 'Chat'), + BottomNavigationBarItem(icon: Icon(Icons.assignment), label: 'Records'), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + ], + currentIndex: 0, + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.grey, + onTap: (index) { + if (index == 3) { + Navigator.pushNamed(context, RouteNames.patientprofileScreen); + } + }, + ); + } +} diff --git a/lib/screens/patientScreens/patientDashboard/patient_profile_screen.dart b/lib/screens/patientScreens/patientDashboard/patient_profile_screen.dart new file mode 100644 index 0000000..c091bc7 --- /dev/null +++ b/lib/screens/patientScreens/patientDashboard/patient_profile_screen.dart @@ -0,0 +1,205 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:telemednet/route_names.dart'; + +class PatientProfileScreen extends StatefulWidget { + const PatientProfileScreen({super.key}); + + @override + State createState() => _PatientProfileScreenState(); +} + +class _PatientProfileScreenState extends State { + final FirebaseAuth _auth = FirebaseAuth.instance; + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + _buildProfileHeader(), + _buildProfileOptions(), + const Spacer(), + _buildBottomNavBar(), + ], + ), + ), + ); + } + + 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: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Center( + child: Text( + 'D', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ), + ), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Dhansh A S', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + 'User profile is incomplete', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), + ), + 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: () {}, + ), + 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, + ); + } + + Widget _buildBottomNavBar() { + return BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home_outlined), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.people_outline), + label: 'Consult', + ), + BottomNavigationBarItem( + icon: Icon(Icons.description_outlined), + label: 'Records', + ), + BottomNavigationBarItem( + icon: Icon(Icons.person), + label: 'Profile', + ), + ], + currentIndex: 3, + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.grey, + onTap: (index) { + if (index != 3) { + Navigator.of(context).pop(); + } + }, + ); + } + + Future _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.')), + ); + } + } + } +} diff --git a/lib/screens/patientScreens/patient_landing_screen.dart b/lib/screens/patientScreens/patient_landing_screen.dart new file mode 100644 index 0000000..e6f8b7d --- /dev/null +++ b/lib/screens/patientScreens/patient_landing_screen.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/route_names.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), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/patientScreens/registrationScreens/family_members_edit_screen.dart b/lib/screens/patientScreens/registrationScreens/family_members_edit_screen.dart new file mode 100644 index 0000000..5a603ba --- /dev/null +++ b/lib/screens/patientScreens/registrationScreens/family_members_edit_screen.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/controller/patient_controller.dart'; +import 'package:telemednet/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 createState() => + _FamilyMembersEditScreenState(); +} + +class _FamilyMembersEditScreenState extends State { + late TextEditingController nameController; + late TextEditingController relationController; + late TextEditingController genderController; + late TextEditingController dobController; + Map 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( + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: Colors.blue), + border: const OutlineInputBorder(), + ), + value: value.isEmpty ? null : value, + onChanged: onChanged, + items: label == 'Relation' + ? ['Father', 'Mother', 'Son', 'Daughter', 'Other'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList() + : ['Male', 'Female', 'Other'] + .map>((String value) { + return DropdownMenuItem( + 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 _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(); + } +} diff --git a/lib/screens/patientScreens/registrationScreens/patient_adress_screen.dart b/lib/screens/patientScreens/registrationScreens/patient_adress_screen.dart new file mode 100644 index 0000000..fde2d63 --- /dev/null +++ b/lib/screens/patientScreens/registrationScreens/patient_adress_screen.dart @@ -0,0 +1,381 @@ +import 'package:flutter/material.dart'; +import 'package:country_state_city_picker/country_state_city_picker.dart'; +import 'package:telemednet/controller/patient_controller.dart'; + +class PatientAddressScreen extends StatefulWidget { + final PatientController? controller; + + const PatientAddressScreen({super.key, required this.controller}); + + @override + State createState() => _PatientAddressScreenState(); +} + +class _PatientAddressScreenState extends State { + 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; + Map _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(); + } +} diff --git a/lib/screens/patientScreens/registrationScreens/patient_family_members_screen.dart b/lib/screens/patientScreens/registrationScreens/patient_family_members_screen.dart new file mode 100644 index 0000000..af39926 --- /dev/null +++ b/lib/screens/patientScreens/registrationScreens/patient_family_members_screen.dart @@ -0,0 +1,308 @@ +import 'package:flutter/material.dart'; +import 'package:telemednet/screens/patientScreens/registrationScreens/family_members_edit_screen.dart'; +import 'package:telemednet/data/models/patient.dart'; +import '../../../controller/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 createState() => + _PatientFamilyMembersScreenState(); +} + +class _PatientFamilyMembersScreenState + extends State { + 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 _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), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/patientScreens/registrationScreens/patient_registration_screen.dart b/lib/screens/patientScreens/registrationScreens/patient_registration_screen.dart new file mode 100644 index 0000000..005e204 --- /dev/null +++ b/lib/screens/patientScreens/registrationScreens/patient_registration_screen.dart @@ -0,0 +1,684 @@ +// ignore_for_file: dead_code + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:telemednet/route_names.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'dart:io'; +import '../../../controller/patient_controller.dart'; +import '../../../widgets/alert_screen.dart'; + +class PatientRegistrationScreen extends StatefulWidget { + const PatientRegistrationScreen({super.key}); + + @override + State createState() => + _PatientRegistrationScreenState(); +} + +class _PatientRegistrationScreenState extends State { + final PatientController _controller = PatientController(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _phoneController = TextEditingController(); + bool _hasErrors = false; + final Map _errors = {}; + + String? _gender; + DateTime? _dateOfBirth; + File? _image; + final ImagePicker _picker = ImagePicker(); + String _selectedCountryCode = '+1'; + + final List _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 _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: [ + 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( + value: _selectedCountryCode, + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedCountryCode = newValue; + }); + + _updateCombinedPhoneNumber( + _phoneController.text); + } + }, + items: + _countryCodes.map>( + (String code) { + return DropdownMenuItem( + 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( + value: _gender, + isExpanded: true, + hint: const Text('Select gender'), + onChanged: (value) { + setState(() => _gender = value); + _controller.updateGender(value!); + }, + items: ['Male', 'Female', 'Other'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ), + _buildUniformField( + label: 'Date of Birth', + icon: Icons.calendar_today_outlined, + child: InkWell( + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _dateOfBirth ?? + DateTime.now() + .subtract(const Duration(days: 365 * 18)), + firstDate: DateTime(1900), + lastDate: DateTime.now() + .subtract(const Duration(days: 365 * 18)), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: Colors.blue), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _dateOfBirth) { + setState(() => _dateOfBirth = picked); + _controller.updateDateOfBirth(picked); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + _dateOfBirth != null + ? DateFormat('dd/MM/yyyy').format(_dateOfBirth!) + : 'Select date of birth', + style: TextStyle( + color: _dateOfBirth != null + ? Colors.black87 + : Colors.grey, + ), + ), + ), + ), + ), + ], + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildNavigationField( + 'Address', + Icons.location_on, + () async { + final result = await Navigator.pushNamed( + context, + RouteNames.patientAdressScreen, + arguments: _controller, + ); + if (result == true) { + setState(() {}); + } + }, + ), + const Divider(height: 1), + _buildNavigationField( + 'Family Members', + Icons.family_restroom_outlined, + () => Navigator.pushNamed( + context, + RouteNames.patientFamilyMembersScreen, + arguments: _controller, + ), + ), + ], + )), + ], + ), + ), + ); + } + + Widget _buildUniformField({ + required String label, + required IconData icon, + required Widget child, + String? errorKey, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.grey.shade200, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 8), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.grey[600], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Row( + children: [ + Icon( + icon, + size: 20, + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.blue, + ), + const SizedBox(width: 12), + Expanded(child: child), + ], + ), + ), + if (_errors.containsKey(errorKey ?? '')) + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Text( + _errors[errorKey]!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + bool _validateAllFields() { + setState(() { + _errors.clear(); + _hasErrors = false; + + final name = _nameController.text.trim(); + if (name.isEmpty) { + _errors['name'] = 'Name is required'; + _hasErrors = true; + } else if (name.length < 2 && + RegExp(r'^[A-Za-z]+([.\s]?[A-Za-z]+)*$').hasMatch(name)) { + _errors['name'] = 'Name must be at least 2 characters'; + _hasErrors = true; + } + + final phoneNumber = _phoneController.text.trim(); + if (phoneNumber.isEmpty) { + _errors['phone'] = 'Phone number is required'; + _hasErrors = true; + } else if (!RegExp(r'^\d{10}$').hasMatch(phoneNumber)) { + _errors['phone'] = 'Enter a valid 10-digit phone number'; + _hasErrors = true; + } + + if (_gender == null) { + _errors['gender'] = 'Please select a gender'; + _hasErrors = true; + } + + if (_dateOfBirth == null) { + _errors['dob'] = 'Date of Birth is required'; + _hasErrors = true; + } else { + final age = DateTime.now().difference(_dateOfBirth!).inDays ~/ 365; + if (age < 18) { + _errors['dob'] = 'User must be at least 18 years old'; + _hasErrors = true; + } + } + + if (_image == null) { + _errors['image'] = 'Profile picture is required'; + _hasErrors = true; + } + + // Validate required address fields + final address = _controller.model.address; + if (address.houseNo?.isEmpty ?? true) { + _errors['address'] = 'Please complete all required address fields'; + _hasErrors = true; + } + + // Validate address type and other label + if (address.addressType == 'Other' && + (address.otherLabel?.isEmpty ?? true)) { + _errors['address'] = 'Please specify other address label'; + _hasErrors = true; + } + }); + + return !_hasErrors; + } + + void _showValidationErrors() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 8), + Text('Validation Errors'), + ], + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: _errors.entries + .map((error) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + '• ${error.value}', + style: const TextStyle(color: Colors.red), + ), + )) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + Widget _buildNavigationField( + String label, IconData icon, VoidCallback onTap) { + bool isAddressField = label == 'Address'; + bool hasAddressError = _errors.containsKey('address'); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (isAddressField && hasAddressError) + ? Colors.red.withOpacity(0.1) + : Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, + color: (isAddressField && hasAddressError) + ? Colors.red + : Colors.blue, + size: 24), + ), + title: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: (isAddressField && hasAddressError) + ? Colors.red + : Colors.black, + ), + ), + subtitle: isAddressField ? _buildAddressSubtitle() : null, + trailing: Icon( + Icons.chevron_right, + color: + (isAddressField && hasAddressError) ? Colors.red : Colors.blue, + ), + onTap: onTap, + ), + if (isAddressField && hasAddressError) + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Text( + _errors['address']!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), + ], + ); + } + + Widget _buildAddressSubtitle() { + final address = _controller.model.address; + if (address.houseNo == null || + address.line == null || + address.city == null) { + return const Text( + 'No address added', + style: TextStyle(color: Colors.grey), + ); + } + + return Text( + '${address.houseNo}, ${address.line}\n' + '${address.city}, ${address.state} ${address.pincode}\n' + '${address.addressType}${address.addressType == "Other" ? ": ${address.otherLabel}" : ""}', + style: const TextStyle(color: Colors.black87), + ); + } +} diff --git a/lib/screens/user_screen.dart b/lib/screens/user_screen.dart deleted file mode 100644 index 6c34f71..0000000 --- a/lib/screens/user_screen.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class UserScreen extends StatelessWidget { - const UserScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('User Screen'), - ), - body: const Center( - child: Text('User Screen'), - ), - ); - } -} diff --git a/lib/services/data_service.dart b/lib/services/data_service.dart new file mode 100644 index 0000000..b9f78bb --- /dev/null +++ b/lib/services/data_service.dart @@ -0,0 +1 @@ +// TODO Implement this library. \ No newline at end of file diff --git a/lib/shared/user_selection.dart b/lib/shared/user_selection.dart deleted file mode 100644 index 38cf0d5..0000000 --- a/lib/shared/user_selection.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../route_names.dart'; - -class UserSelection extends StatefulWidget { - const UserSelection({super.key}); - - @override - State createState() => _UserSelectionState(); -} - -class _UserSelectionState extends State { - String _selectedUserType = ''; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'Register', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - const Text( - 'Select User Type', - style: TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - _buildSelectionOption( - icon: Icons.medical_services, - label: 'Doctor', - description: 'Can organise and approve appointments', - onTap: () => _selectUserType('Doctor'), - ), - const SizedBox(height: 12), - _buildSelectionOption( - icon: Icons.person, - label: 'Patient', - description: 'Can book appointments', - onTap: () => _selectUserType('Patient'), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - if (_selectedUserType == 'Patient') { - Navigator.of(context) - .pushNamed(RouteNames.patientLandingScreen); - } else {} - }, - child: const Text('Next'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildSelectionOption({ - required IconData icon, - required String label, - required String description, - required VoidCallback onTap, - }) { - final isSelected = _selectedUserType == label; - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all( - color: isSelected ? Colors.blue : Colors.grey[300]!, - width: 2, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon(icon, size: 40, color: Colors.blue), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.bold), - ), - Text( - description, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ], - ), - ), - if (isSelected) const Icon(Icons.check_circle, color: Colors.blue), - ], - ), - ), - ); - } - - void _selectUserType(String userType) { - setState(() { - _selectedUserType = userType; - }); - } -} diff --git a/lib/widgets/alert_screen.dart b/lib/widgets/alert_screen.dart new file mode 100644 index 0000000..6850241 --- /dev/null +++ b/lib/widgets/alert_screen.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +enum AlertType { success, error, info } + +class AlertArguments { + final String title; + final String message; + final String actionTitle; + final AlertType type; + final VoidCallback onActionPressed; + + AlertArguments({ + this.title = '', + required this.message, + required this.actionTitle, + required this.type, + required this.onActionPressed, + }); +} + +class AlertScreen extends StatelessWidget { + final AlertArguments arguments; + + const AlertScreen({super.key, required this.arguments}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + arguments.type == AlertType.success + ? Icons.thumb_up_outlined + : arguments.type == AlertType.error + ? Icons.thumb_down_outlined + : Icons.info_outline, + size: 150, + color: arguments.type == AlertType.error + ? Colors.red + : arguments.type == AlertType.success + ? Colors.lightBlue + : Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 32), + if (arguments.title.isNotEmpty) ...[ + Text( + arguments.title, + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ], + Text( + arguments.message, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + TextButton( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + onPressed: arguments.onActionPressed, + child: Text( + arguments.actionTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: arguments.type == AlertType.error + ? Colors.red + : Colors.lightBlue, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/error_view.dart b/lib/widgets/error_view.dart deleted file mode 100644 index 7acb9f6..0000000 --- a/lib/widgets/error_view.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class ErrorView extends StatefulWidget { - final String message; - final String okMessage; - final Function() onPressed; - - const ErrorView( - {super.key, - required this.message, - required this.okMessage, - required this.onPressed}); - - @override - State createState() => _ErrorViewState(); -} - -class _ErrorViewState extends State { - @override - Widget build(BuildContext context) { - return Center( - child: Column( - children: [ - Text(widget.message), - ElevatedButton( - onPressed: widget.onPressed, child: Text(widget.okMessage)) - ], - ), - ); - } -} diff --git a/lib/widgets/primary_button.dart b/lib/widgets/primary_button.dart index 2791f3a..360975a 100644 --- a/lib/widgets/primary_button.dart +++ b/lib/widgets/primary_button.dart @@ -3,14 +3,15 @@ import 'package:flutter/material.dart'; class PrimaryButton extends StatelessWidget { final String text; final void Function()? onPressed; + final IconData? icon; - const PrimaryButton({super.key, required this.text, required this.onPressed}); + const PrimaryButton( + {super.key, required this.text, required this.onPressed, this.icon}); @override Widget build(BuildContext context) { return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary), + style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), onPressed: onPressed, child: Text(text, style: TextStyle(color: Theme.of(context).colorScheme.onPrimary))); diff --git a/pubspec.lock b/pubspec.lock index 85cab23..8779588 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + country_state_city: + dependency: "direct main" + description: + name: country_state_city + sha256: "400bf4f455503021d98a1f21eeec41c0aa0d7b2529ea0b07f6799ed3750513c3" + url: "https://pub.dev" + source: hosted + version: "0.1.6" country_state_city_picker: dependency: "direct main" description: @@ -339,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.23" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" + url: "https://pub.dev" + source: hosted + version: "3.1.1" flutter_svg: dependency: transitive description: @@ -365,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" http: dependency: transitive description: @@ -453,6 +477,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + intl_phone_field: + dependency: "direct main" + description: + name: intl_phone_field + sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf" + url: "https://pub.dev" + source: hosted + version: "3.2.0" json_annotation: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3ae3820..46d26cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.2.1+4 environment: sdk: ^3.5.3 @@ -44,7 +44,11 @@ dependencies: image_picker: ^1.1.2 fhir: ^0.12.0 intl: ^0.19.0 + country_state_city: ^0.1.6 country_state_city_picker: ^1.2.8 + intl_phone_field: ^3.2.0 + flutter_slidable: ^3.1.1 + gap: ^3.0.1 dev_dependencies: flutter_test: