Patient registration complete with authentication flow fixed (#3)

Patient registration complete with authentication flow fixed

Co-authored-by: Benoy Bose <benoybose@gmail.com>
Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com>
Reviewed-on: cosqnet/telemednet#3
Reviewed-by: Benoy Bose <benoybose@cosq.net>
Co-authored-by: DhanshCOSQ <dhanshas@cosq.net>
Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
This commit is contained in:
Dhansh A S 2024-10-31 14:20:35 +00:00 committed by Benoy Bose
parent ec433190c4
commit 66c3b2fb9c
43 changed files with 5654 additions and 423 deletions

2
.env
View File

@ -1,2 +1,4 @@
CUSTOM_SCHEME=com.cosqnet.telemednet
PROFILE_COLLECTION_NAME=telemednetusers
PATIENT_PROFILE_COLLECTION_NAME=patientprofiles
DOCTOR_PROFILE_COLLECTION_NAME=doctorprofiles

View File

@ -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

View File

@ -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
}

View File

@ -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
);

View File

@ -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);
}
}

View File

@ -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<String, String> validationErrors = {};
void clearValidationErrors() {
validationErrors.clear();
}
void updateName(String name) {
model.name = name;
}
void updatePhoneNumber(String phoneNumber) {
model.phoneNumber = phoneNumber;
}
void updateGender(String gender) {
model.gender = gender;
}
void updateDateOfBirth(DateTime dateOfBirth) {
model.dateOfBirth = dateOfBirth;
}
void updateProfileImage(String imagePath) {
model.profileImagePath = imagePath;
}
void updateHouseNo(String houseNo) {
model.address.houseNo = houseNo;
}
void updateLine(String line) {
model.address.line = line;
}
void updateTown(String town) {
model.address.town = town;
}
void updatePincode(String pincode) {
model.address.pincode = pincode;
}
void updateCountry(String country) {
model.address.country = country;
}
void updateState(String state) {
model.address.state = state;
}
void updateCity(String city) {
model.address.city = city;
}
void updateAddressType(String addressType) {
model.address.addressType = addressType;
}
void updateOtherLabel(String otherLabel) {
model.address.otherLabel = otherLabel;
}
void addFamilyMember(FamilyMember member) {
model.familyMembers.add(member);
}
void updateFamilyMember(int index, FamilyMember member) {
if (index >= 0 && index < model.familyMembers.length) {
model.familyMembers[index] = member;
}
}
void deleteFamilyMember(int index) {
if (index >= 0 && index < model.familyMembers.length) {
model.familyMembers.removeAt(index);
}
}
Future<bool> savePatientData() async {
return await PatientProfileService.savePatientProfile(this);
}
Future<bool> loadPatientData() async {
PatientModel? loadedModel = await PatientProfileService.getPatientProfile();
if (loadedModel != null) {
model.updateFrom(loadedModel);
return true;
}
return false;
}
Future<bool> updatePatientData() async {
return await PatientProfileService.updatePatientProfile(this);
}
Future<bool> deletePatientData() async {
return await PatientProfileService.deletePatientProfile();
}
}

View File

@ -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<String> 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;
// }
}

View File

@ -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<TelemedUser?> 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;
}
}
}

116
lib/data/models/doctor.dart Normal file
View File

@ -0,0 +1,116 @@
class Doctor {
// Add achievements field
List<String> 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<String, dynamic> toJson() => {
'achievements': achievements,
'speciality': speciality,
'yearsOfExperience': yearsOfExperience,
'licenseNumber': licenseNumber,
'profileDescription': profileDescription,
'digitalSignature': digitalSignature,
'address': address.toJson(),
'profile': profile.toJson(),
};
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
achievements: List<String>.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<String> qualifications;
var profileDescription;
Profile({
this.title,
this.surName,
this.middleName,
this.lastName,
required this.qualifications,
});
Map<String, dynamic> toJson() => {
'title': title,
'surname': surName,
'lastName': lastName,
'middleName': middleName,
'qualifications': qualifications,
};
static Profile fromJson(Map<String, dynamic> json) => Profile(
title: json['title'],
surName: json['surname'],
middleName: json['middleName'],
lastName: json['lastName'],
qualifications: List<String>.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<String, dynamic> toJson() => {
'floorBuilding': floorBuilding,
'street': street,
'city': city,
'state': state,
'country': country,
'postalCode': postalCode,
};
static Address fromJson(Map<String, dynamic> json) => Address(
floorBuilding: json['floorBuilding'],
street: json['street'],
city: json['city'],
state: json['state'],
country: json['country'],
postalCode: json['postalCode'],
);
}

View File

@ -1 +1,144 @@
class PatientModel {
String? name;
String? phoneNumber;
String? gender;
DateTime? dateOfBirth;
String? profileImagePath;
PatientAddress address;
List<FamilyMember> familyMembers = [];
PatientModel() : address = PatientAddress();
Map<String, dynamic> 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<String, dynamic> json)
: address = PatientAddress() {
name = json['name'];
phoneNumber = json['phoneNumber'];
gender = json['gender'];
dateOfBirth = json['dateOfBirth'] != null
? DateTime.parse(json['dateOfBirth'])
: null;
profileImagePath = json['profileImagePath'];
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<String, dynamic> toJson() {
return {
'name': name,
'relation': relation,
'gender': gender,
'dateOfBirth': dateOfBirth?.toIso8601String(),
};
}
FamilyMember.fromJson(Map<String, dynamic> json) {
name = json['name'];
relation = json['relation'];
gender = json['gender'];
dateOfBirth = json['dateOfBirth'] != null
? DateTime.parse(json['dateOfBirth'])
: null;
}
}
class PatientAddress {
String? houseNo;
String? line;
String? town;
String? pincode;
String? country;
String? state;
String? city;
String? addressType;
String? otherLabel;
PatientAddress({
this.houseNo,
this.line,
this.town,
this.pincode,
this.country,
this.state,
this.city,
this.addressType,
this.otherLabel,
});
Map<String, dynamic> toJson() {
return {
'houseNo': houseNo,
'line': line,
'town': town,
'pincode': pincode,
'country': country,
'state': state,
'city': city,
'addressType': addressType,
'otherLabel': otherLabel,
};
}
PatientAddress.fromJson(Map<String, dynamic> json) {
houseNo = json['houseNo'];
line = json['line'];
town = json['town'];
pincode = json['pincode'];
country = json['country'];
state = json['state'];
city = json['city'];
addressType = json['addressType'];
otherLabel = json['otherLabel'];
}
}

View File

@ -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<String, dynamic> 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<String, dynamic> json, String userId) {
uid = userId;
email = json['email'] as String?;
phoneNumber = json['phoneNumber'] as String?;
role = (json['UserType'] ?? json['role']) as String;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
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,
};
}
}

View File

@ -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<Map<String, dynamic>> 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<TelemedUser?> 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;
}
}
}

View File

@ -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<String, dynamic> 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<Doctor?> 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<String, dynamic>;
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<String, dynamic> 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;
}
}
}

View File

@ -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<void> 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<void> handleDoctorNavigation(BuildContext context) async {
if (context.mounted) {
Navigator.pushReplacementNamed(
context,
RouteNames.profileUpload,
);
}
}
static Future<void> handlePatientNavigation(BuildContext context) async {
try {
final patientProfile = await PatientProfileService.getPatientProfile();
if (context.mounted) {
if (patientProfile != null) {
Navigator.pushReplacementNamed(
context, RouteNames.patientDashboardScreen);
} else {
Navigator.pushReplacementNamed(
context, RouteNames.patientLandingScreen);
}
}
} catch (e) {
print('Error in handlePatientNavigation: $e');
if (context.mounted) {
Navigator.pushReplacementNamed(
context, RouteNames.patientLandingScreen);
}
}
}
}

View File

@ -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<bool> savePatientProfile(PatientController controller) async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return false;
}
final String uid = user.uid;
final PatientModel patientData = controller.model;
final Map<String, dynamic> 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<PatientModel?> getPatientProfile() async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return null;
}
final String uid = user.uid;
final DocumentSnapshot doc =
await db.collection(patientProfileCollectionName).doc(uid).get();
if (!doc.exists) {
print('No patient profile found for this user');
return null;
}
final data = doc.data() as Map<String, dynamic>;
return PatientModel.fromJson(data);
} catch (e) {
print('Error fetching patient profile: $e');
return null;
}
}
static Future<bool> 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<String, dynamic> 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<bool> deletePatientProfile() async {
try {
final User? user = FirebaseAuth.instance.currentUser;
if (user == null) {
print('No user logged in');
return false;
}
final String uid = user.uid;
await db.collection(patientProfileCollectionName).doc(uid).delete();
print('Patient profile deleted successfully');
return true;
} catch (e) {
print('Error deleting patient profile: $e');
return false;
}
}
}

View File

@ -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';
}

View File

@ -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<String, Widget Function(BuildContext)> routes = {
RouteNames.launch: (context) => const LaunchScreen(),
@ -20,19 +26,53 @@ final Map<String, Widget Function(BuildContext)> 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()
};

View File

@ -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<LaunchScreen> {
String? selectedUserType;
@override
void initState() {
super.initState();
@ -22,102 +24,198 @@ class _LaunchScreenState extends State<LaunchScreen> {
@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<void> _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<void> _fetchProfileAndNavigate(BuildContext context) async {
if (mounted) {
await NavigationService.handleUserNavigation(context);
}
}
}

View File

@ -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<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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<void> _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();
}
}

View File

@ -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<UserProfileScreen> createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State<UserProfileScreen> {
TelemedUser? user;
final FirebaseAuth _auth = FirebaseAuth.instance;
@override
void initState() {
super.initState();
user = widget.user;
}
Future<void> _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'),
),
],
),
);
}
}

View File

@ -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<ProfileUploadPage> {
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<void> _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,
),
);
}
}

View File

@ -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<AchievementsScreen> createState() => _AchievementsScreenState();
}
class _AchievementsScreenState extends State<AchievementsScreen> {
final TextEditingController _achievementController = TextEditingController();
late final DoctorController _controller;
late List<String> 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<String>.from(_controller.model.achievements);
}
void _addAchievement() {
final achievement = _achievementController.text.trim();
if (achievement.isNotEmpty) {
setState(() {
achievements.add(achievement);
_isEditing = true;
});
_controller.updateAchievements(List<String>.from(achievements));
_achievementController.clear();
} else {
_showError('Please enter an achievement');
}
}
void _removeAchievement(int index) {
setState(() {
achievements.removeAt(index);
_isEditing = true;
});
_controller.updateAchievements(List<String>.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<bool>(
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,
),
),
],
),
),
],
),
),
);
}
}

View File

@ -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<DoctorAddressScreen> createState() => _DoctorAddressScreenState();
}
class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
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(),
),
),
);
}
}

View File

@ -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<DigitalSignatureScreen> createState() => _DigitalSignatureScreenState();
// // }
// // class _DigitalSignatureScreenState extends State<DigitalSignatureScreen> {
// // File? _signatureFile;
// // final ImagePicker _picker = ImagePicker();
// // Future<void> _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<DigitalSignatureScreen> createState() => _DigitalSignatureScreenState();
// }
// class _DigitalSignatureScreenState extends State<DigitalSignatureScreen> {
// File? _signatureFile;
// final ImagePicker _picker = ImagePicker();
// Future<void> _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<DigitalSignatureScreen> createState() => _DigitalSignatureScreenState();
}
class _DigitalSignatureScreenState extends State<DigitalSignatureScreen> {
File? _signatureFile;
final ImagePicker _picker = ImagePicker();
Future<void> _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<void> _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,
),
),
),
],
),
);
}
}

View File

@ -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<ExperienceScreen> {
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<bool>(
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<String>(
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,
),
),
],
),
),
),
);
}
}

View File

@ -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<ProfileDescriptionScreen> {
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<bool>(
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,
),
),
],
),
),
),
);
}
}

View File

@ -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<SpecialitiesScreen> createState() => _SpecialitiesScreenState();
}
class _SpecialitiesScreenState extends State<SpecialitiesScreen> {
String? selectedSpeciality;
late final DoctorController _controller;
bool _isEditing = false;
final List<Map<String, dynamic>> 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<bool>(
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,
),
),
],
),
),
],
),
),
);
}
}

View File

@ -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) {

View File

@ -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<PatientDashboardScreen> createState() => _PatientDashboardScreenState();
}
class _PatientDashboardScreenState extends State<PatientDashboardScreen> {
@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);
}
},
);
}
}

View File

@ -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<PatientProfileScreen> createState() => _PatientProfileScreenState();
}
class _PatientProfileScreenState extends State<PatientProfileScreen> {
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<void> _signOut() async {
try {
await _auth.signOut();
if (mounted) {
Navigator.of(context).pushReplacementNamed(RouteNames.launch);
}
} catch (e) {
print("Error signing out: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to log out. Please try again.')),
);
}
}
}
}

View File

@ -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),
),
),
],
),
),
),
),
),
],
),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
),
);
}
}

View File

@ -0,0 +1 @@
// TODO Implement this library.

View File

@ -1,120 +0,0 @@
import 'package:flutter/material.dart';
import '../route_names.dart';
class UserSelection extends StatefulWidget {
const UserSelection({super.key});
@override
State<UserSelection> createState() => _UserSelectionState();
}
class _UserSelectionState extends State<UserSelection> {
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;
});
}
}

View File

@ -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,
),
),
),
],
),
),
),
);
}
}

View File

@ -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<ErrorView> createState() => _ErrorViewState();
}
class _ErrorViewState extends State<ErrorView> {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
Text(widget.message),
ElevatedButton(
onPressed: widget.onPressed, child: Text(widget.okMessage))
],
),
);
}
}

View File

@ -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)));

View File

@ -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:

View File

@ -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: