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
|
CUSTOM_SCHEME=com.cosqnet.telemednet
|
||||||
PROFILE_COLLECTION_NAME=telemednetusers
|
PROFILE_COLLECTION_NAME=telemednetusers
|
||||||
|
PATIENT_PROFILE_COLLECTION_NAME=patientprofiles
|
||||||
|
DOCTOR_PROFILE_COLLECTION_NAME=doctorprofiles
|
||||||
@ -16,7 +16,7 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|||||||
android {
|
android {
|
||||||
namespace = "com.cosqnet.telemednet"
|
namespace = "com.cosqnet.telemednet"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "25.1.8937393"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
@ -28,10 +28,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "com.cosqnet.telemednet"
|
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
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
|
|||||||
@ -18,10 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.1.0" apply false
|
id "com.android.application" version "8.3.2" apply false
|
||||||
// START: FlutterFire Configuration
|
|
||||||
id "com.google.gms.google-services" version "4.3.15" 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
|
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 {
|
class TelemedUser {
|
||||||
String uid;
|
late String uid;
|
||||||
late String? name;
|
String? email;
|
||||||
late String? email;
|
String? phoneNumber;
|
||||||
late String? photoURL;
|
late String role;
|
||||||
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});
|
|
||||||
|
|
||||||
TelemedUser.fromJson(Map<String, dynamic> json, this.uid) {
|
TelemedUser({
|
||||||
uid = json['uid'];
|
required this.uid,
|
||||||
name = json['name'];
|
this.email,
|
||||||
email = json['email'];
|
this.phoneNumber,
|
||||||
photoURL = json['photoURL'];
|
required this.role,
|
||||||
phoneNumber = json['phoneNumber'];
|
});
|
||||||
alterPhoneNumber = json['alterPhoneNumber'];
|
|
||||||
role = json['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() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
return {
|
||||||
data['uid'] = uid;
|
'uid': uid,
|
||||||
data['name'] = name;
|
'email': email,
|
||||||
data['email'] = email;
|
'phoneNumber': phoneNumber,
|
||||||
data['photoURL'] = photoURL;
|
'UserType': role,
|
||||||
data['phoneNumber'] = phoneNumber;
|
};
|
||||||
data['alterPhoneNumber'] = alterPhoneNumber;
|
|
||||||
data['role'] = role;
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 userHome = '/user-home';
|
||||||
static const String signUp = '/sign-up';
|
static const String signUp = '/sign-up';
|
||||||
static const String launch = '/launch';
|
static const String launch = '/launch';
|
||||||
|
static const String profileUpload = '/profile-upload';
|
||||||
static const String patientLandingScreen = '/patient-landing-screen';
|
static const String patientLandingScreen = '/patient-landing-screen';
|
||||||
static const String patientDashboardScreen = '/patient-dahboard-screen';
|
static const String patientDashboardScreen = '/patient-dahboard-screen';
|
||||||
static const String patientRegistrationScreen =
|
static const String patientRegistrationScreen =
|
||||||
@ -12,4 +13,11 @@ class RouteNames {
|
|||||||
static const String patientFamilyMembersScreen =
|
static const String patientFamilyMembersScreen =
|
||||||
'/patient-family-members-screen';
|
'/patient-family-members-screen';
|
||||||
static const String familyMembersEditScreen = '/family-members-edit-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:firebase_ui_auth/firebase_ui_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:telemednet/data/models/telemed_user.dart';
|
import 'package:telemednet/screens/authentication/launch_screen.dart';
|
||||||
import 'package:telemednet/screens/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/route_names.dart';
|
||||||
import 'package:telemednet/screens/patientDashboard/patient_dashboard_screen.dart';
|
import 'package:telemednet/screens/doctor_screens/digital_signature.dart';
|
||||||
import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_adress_screen.dart';
|
import 'package:telemednet/screens/doctor_screens/experience_screen.dart';
|
||||||
import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_family_members_screen.dart';
|
import 'package:telemednet/screens/doctor_screens/profile_description_screen.dart';
|
||||||
import 'package:telemednet/screens/patientDashboard/registrationScreens/patient_registration_screen.dart';
|
import 'package:telemednet/screens/doctor_screens/specialities_selection.dart';
|
||||||
import 'package:telemednet/screens/user_profile_screen.dart';
|
import 'package:telemednet/screens/patientScreens/patientDashboard/patient_dashboard_screen.dart';
|
||||||
import 'package:telemednet/screens/user_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 'controllers/doctor _controller.dart';
|
||||||
import 'screens/patientDashboard/registrationScreens/family_members_edit_screen.dart';
|
import 'screens/patientScreens/patient_landing_screen.dart';
|
||||||
|
import 'screens/patientScreens/registrationScreens/family_members_edit_screen.dart';
|
||||||
|
|
||||||
final Map<String, Widget Function(BuildContext)> routes = {
|
final Map<String, Widget Function(BuildContext)> routes = {
|
||||||
RouteNames.launch: (context) => const LaunchScreen(),
|
RouteNames.launch: (context) => const LaunchScreen(),
|
||||||
@ -20,19 +26,53 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
|||||||
providers: [EmailAuthProvider(), PhoneAuthProvider()],
|
providers: [EmailAuthProvider(), PhoneAuthProvider()],
|
||||||
),
|
),
|
||||||
RouteNames.signUp: (context) => const RegisterScreen(),
|
RouteNames.signUp: (context) => const RegisterScreen(),
|
||||||
RouteNames.userProfile: (context) {
|
// RouteNames.userProfile: (context) {
|
||||||
var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?;
|
// var user = ModalRoute.of(context)!.settings.arguments as TelemedUser?;
|
||||||
return UserProfileScreen(user: user);
|
// return UserProfileScreen(user: user);
|
||||||
},
|
// },
|
||||||
RouteNames.userHome: (context) => const UserScreen(),
|
RouteNames.profileUpload: (context) => ProfileUploadPage(),
|
||||||
RouteNames.patientLandingScreen: (context) => const PatientLandingScreen(),
|
RouteNames.patientLandingScreen: (context) => const PatientLandingScreen(),
|
||||||
RouteNames.patientDashboardScreen: (context) =>
|
RouteNames.patientDashboardScreen: (context) =>
|
||||||
const PatientDashboardScreen(),
|
const PatientDashboardScreen(),
|
||||||
RouteNames.patientRegistrationScreen: (context) =>
|
RouteNames.patientRegistrationScreen: (context) =>
|
||||||
const PatientRegistrationScreen(),
|
const PatientRegistrationScreen(),
|
||||||
RouteNames.patientAdressScreen: (context) => const PatientAddressScreen(),
|
// RouteNames.patientAdressScreen: (context) =>
|
||||||
RouteNames.patientFamilyMembersScreen: (context) =>
|
// PatientAddressScreen(controller: PatientController()),
|
||||||
const PatientFamilyMembersScreen(),
|
// RouteNames.patientFamilyMembersScreen: (context) =>
|
||||||
RouteNames.familyMembersEditScreen: (context) =>
|
// PatientFamilyMembersScreen(controller: PatientController()),
|
||||||
const FamilyMembersEditScreen(),
|
// 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 'dart:async';
|
||||||
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.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/route_names.dart';
|
||||||
|
import 'package:telemednet/screens/authentication/sign_up_screen.dart';
|
||||||
import 'package:telemednet/widgets/primary_button.dart';
|
import 'package:telemednet/widgets/primary_button.dart';
|
||||||
|
|
||||||
class LaunchScreen extends StatefulWidget {
|
class LaunchScreen extends StatefulWidget {
|
||||||
@ -14,6 +14,8 @@ class LaunchScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LaunchScreenState extends State<LaunchScreen> {
|
class _LaunchScreenState extends State<LaunchScreen> {
|
||||||
|
String? selectedUserType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -22,102 +24,198 @@ class _LaunchScreenState extends State<LaunchScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('images/cover-picture.jpg'),
|
image: AssetImage('images/cover-picture.jpg'),
|
||||||
fit: BoxFit.cover,
|
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 {
|
Widget _buildLoadingWidget() {
|
||||||
var profile = await DataService.getProfile();
|
return const SizedBox(
|
||||||
if (mounted) {
|
height: 120,
|
||||||
setState(() {
|
child: Center(
|
||||||
if (profile == null) {
|
child: Column(
|
||||||
Navigator.of(context)
|
mainAxisSize: MainAxisSize.min,
|
||||||
.pushReplacementNamed(RouteNames.userProfile, arguments: profile);
|
children: [
|
||||||
} else {
|
CircularProgressIndicator(),
|
||||||
Navigator.of(context)
|
SizedBox(height: 12),
|
||||||
.pushReplacementNamed(RouteNames.userHome, arguments: profile);
|
Text('Please wait...'),
|
||||||
}
|
],
|
||||||
});
|
),
|
||||||
}
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProceedingWidget(BuildContext context) {
|
Widget _buildUserTypeSelection(BuildContext context) {
|
||||||
return const Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
_buildSelectionCard(
|
||||||
SizedBox(height: 10),
|
title: 'Doctor',
|
||||||
Text('Please wait...')
|
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) {
|
Widget _buildSelectionCard({
|
||||||
return Row(
|
required String title,
|
||||||
mainAxisSize: MainAxisSize.max,
|
required String description,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
required IconData icon,
|
||||||
children: [
|
required bool isSelected,
|
||||||
Expanded(
|
required VoidCallback onTap,
|
||||||
child: ElevatedButton(
|
}) {
|
||||||
onPressed: () {
|
return InkWell(
|
||||||
Navigator.of(context).pushNamed(RouteNames.signUp);
|
onTap: onTap,
|
||||||
},
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: const Text('Sign Up'))),
|
child: Container(
|
||||||
const SizedBox(width: 10),
|
decoration: BoxDecoration(
|
||||||
Expanded(
|
border: Border.all(
|
||||||
child: PrimaryButton(
|
color: isSelected ? Colors.blue : Colors.grey.shade300,
|
||||||
onPressed: () {
|
width: isSelected ? 2 : 1,
|
||||||
Navigator.of(context).pushNamed(RouteNames.signIn);
|
),
|
||||||
},
|
borderRadius: BorderRadius.circular(12),
|
||||||
text: 'Sign In',
|
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';
|
import 'package:telemednet/route_names.dart';
|
||||||
|
|
||||||
class PatientLandingScreen extends StatelessWidget {
|
class PatientLandingScreen extends StatelessWidget {
|
||||||
const PatientLandingScreen({Key? key}) : super(key: key);
|
const PatientLandingScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 {
|
class PrimaryButton extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final void Function()? onPressed;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(text,
|
child: Text(text,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)));
|
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)));
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@ -81,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
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:
|
country_state_city_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -339,6 +347,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.23"
|
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:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -365,6 +381,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.4"
|
version: "2.4.4"
|
||||||
|
gap:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: gap
|
||||||
|
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -453,6 +477,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
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:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.5.3
|
sdk: ^3.5.3
|
||||||
@ -44,7 +44,11 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
fhir: ^0.12.0
|
fhir: ^0.12.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
country_state_city: ^0.1.6
|
||||||
country_state_city_picker: ^1.2.8
|
country_state_city_picker: ^1.2.8
|
||||||
|
intl_phone_field: ^3.2.0
|
||||||
|
flutter_slidable: ^3.1.1
|
||||||
|
gap: ^3.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user