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:
parent
ec433190c4
commit
66c3b2fb9c
2
.env
2
.env
@ -1,2 +1,4 @@
|
||||
CUSTOM_SCHEME=com.cosqnet.telemednet
|
||||
PROFILE_COLLECTION_NAME=telemednetusers
|
||||
PATIENT_PROFILE_COLLECTION_NAME=patientprofiles
|
||||
DOCTOR_PROFILE_COLLECTION_NAME=doctorprofiles
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
25
lib/common/color_scheme.dart
Normal file
25
lib/common/color_scheme.dart
Normal 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
|
||||
);
|
||||
124
lib/common/custom_style.dart
Normal file
124
lib/common/custom_style.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
104
lib/controller/patient_controller.dart
Normal file
104
lib/controller/patient_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
115
lib/controllers/doctor _controller.dart
Normal file
115
lib/controllers/doctor _controller.dart
Normal 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;
|
||||
// }
|
||||
}
|
||||
@ -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
116
lib/data/models/doctor.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
92
lib/data/services/data_service.dart
Normal file
92
lib/data/services/data_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
lib/data/services/doctor_profile_service.dart
Normal file
118
lib/data/services/doctor_profile_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
lib/data/services/navigation_service.dart
Normal file
75
lib/data/services/navigation_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
lib/data/services/patient_registration_service.dart
Normal file
113
lib/data/services/patient_registration_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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()
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
227
lib/screens/authentication/sign_up_screen.dart
Normal file
227
lib/screens/authentication/sign_up_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
200
lib/screens/doctor_screens/Doctor_profile_screen.dart
Normal file
200
lib/screens/doctor_screens/Doctor_profile_screen.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
253
lib/screens/doctor_screens/achivements.dart
Normal file
253
lib/screens/doctor_screens/achivements.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
lib/screens/doctor_screens/address_screen.dart
Normal file
168
lib/screens/doctor_screens/address_screen.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
424
lib/screens/doctor_screens/digital_signature.dart
Normal file
424
lib/screens/doctor_screens/digital_signature.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
235
lib/screens/doctor_screens/experience_screen.dart
Normal file
235
lib/screens/doctor_screens/experience_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
244
lib/screens/doctor_screens/profile_description_screen.dart
Normal file
244
lib/screens/doctor_screens/profile_description_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
296
lib/screens/doctor_screens/specialities_selection.dart
Normal file
296
lib/screens/doctor_screens/specialities_selection.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
lib/screens/patientScreens/patient_landing_screen.dart
Normal file
94
lib/screens/patientScreens/patient_landing_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
lib/services/data_service.dart
Normal file
1
lib/services/data_service.dart
Normal file
@ -0,0 +1 @@
|
||||
// TODO Implement this library.
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
93
lib/widgets/alert_screen.dart
Normal file
93
lib/widgets/alert_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)));
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@ -81,6 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
country_state_city:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: country_state_city
|
||||
sha256: "400bf4f455503021d98a1f21eeec41c0aa0d7b2529ea0b07f6799ed3750513c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
country_state_city_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -339,6 +347,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.23"
|
||||
flutter_slidable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -365,6 +381,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
gap:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: gap
|
||||
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -453,6 +477,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
intl_phone_field:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl_phone_field
|
||||
sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user