Split the buisiness logic, to keep only doctor related buisiness
This commit is contained in:
parent
54f05e377d
commit
370107db72
@ -1,104 +0,0 @@
|
|||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
import '../data/services/patient_registration_service.dart';
|
|
||||||
|
|
||||||
class PatientController {
|
|
||||||
final PatientModel model = PatientModel();
|
|
||||||
Map<String, String> validationErrors = {};
|
|
||||||
|
|
||||||
void clearValidationErrors() {
|
|
||||||
validationErrors.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateName(String name) {
|
|
||||||
model.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updatePhoneNumber(String phoneNumber) {
|
|
||||||
model.phoneNumber = phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateGender(String gender) {
|
|
||||||
model.gender = gender;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDateOfBirth(DateTime dateOfBirth) {
|
|
||||||
model.dateOfBirth = dateOfBirth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateProfileImage(String imagePath) {
|
|
||||||
model.profileImagePath = imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateHouseNo(String houseNo) {
|
|
||||||
model.address.houseNo = houseNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateLine(String line) {
|
|
||||||
model.address.line = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateTown(String town) {
|
|
||||||
model.address.town = town;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updatePincode(String pincode) {
|
|
||||||
model.address.pincode = pincode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateCountry(String country) {
|
|
||||||
model.address.country = country;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateState(String state) {
|
|
||||||
model.address.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateCity(String city) {
|
|
||||||
model.address.city = city;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAddressType(String addressType) {
|
|
||||||
model.address.addressType = addressType;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateOtherLabel(String otherLabel) {
|
|
||||||
model.address.otherLabel = otherLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addFamilyMember(FamilyMember member) {
|
|
||||||
model.familyMembers.add(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateFamilyMember(int index, FamilyMember member) {
|
|
||||||
if (index >= 0 && index < model.familyMembers.length) {
|
|
||||||
model.familyMembers[index] = member;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void deleteFamilyMember(int index) {
|
|
||||||
if (index >= 0 && index < model.familyMembers.length) {
|
|
||||||
model.familyMembers.removeAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> savePatientData() async {
|
|
||||||
return await PatientProfileService.savePatientProfile(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> loadPatientData() async {
|
|
||||||
PatientModel? loadedModel = await PatientProfileService.getPatientProfile();
|
|
||||||
if (loadedModel != null) {
|
|
||||||
model.updateFrom(loadedModel);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> updatePatientData() async {
|
|
||||||
return await PatientProfileService.updatePatientProfile(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deletePatientData() async {
|
|
||||||
return await PatientProfileService.deletePatientProfile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
class PatientModel {
|
|
||||||
String? name;
|
|
||||||
String? phoneNumber;
|
|
||||||
String? gender;
|
|
||||||
DateTime? dateOfBirth;
|
|
||||||
String? profileImagePath;
|
|
||||||
String? profileImageUrl;
|
|
||||||
PatientAddress address;
|
|
||||||
|
|
||||||
List<FamilyMember> familyMembers = [];
|
|
||||||
|
|
||||||
PatientModel() : address = PatientAddress();
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'name': name,
|
|
||||||
'phoneNumber': phoneNumber,
|
|
||||||
'gender': gender,
|
|
||||||
'dateOfBirth': dateOfBirth?.toIso8601String(),
|
|
||||||
'profileImagePath': profileImagePath,
|
|
||||||
'profileImageUrl': profileImageUrl,
|
|
||||||
'address': address.toJson(),
|
|
||||||
'familyMembers': familyMembers.map((member) => member.toJson()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
PatientModel.fromJson(Map<String, dynamic> json)
|
|
||||||
: address = PatientAddress() {
|
|
||||||
name = json['name'];
|
|
||||||
phoneNumber = json['phoneNumber'];
|
|
||||||
gender = json['gender'];
|
|
||||||
dateOfBirth = json['dateOfBirth'] != null
|
|
||||||
? DateTime.parse(json['dateOfBirth'])
|
|
||||||
: null;
|
|
||||||
profileImagePath = json['profileImagePath'];
|
|
||||||
profileImageUrl = json['profileImageUrl'];
|
|
||||||
if (json['address'] != null) {
|
|
||||||
address =
|
|
||||||
PatientAddress.fromJson(json['address'] as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
if (json['familyMembers'] != null) {
|
|
||||||
familyMembers = (json['familyMembers'] as List)
|
|
||||||
.map((memberJson) => FamilyMember.fromJson(memberJson))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateFrom(PatientModel other) {
|
|
||||||
name = other.name;
|
|
||||||
phoneNumber = other.phoneNumber;
|
|
||||||
gender = other.gender;
|
|
||||||
dateOfBirth = other.dateOfBirth;
|
|
||||||
profileImagePath = other.profileImagePath;
|
|
||||||
profileImageUrl = other.profileImageUrl;
|
|
||||||
address = other.address;
|
|
||||||
familyMembers = other.familyMembers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FamilyMember {
|
|
||||||
String? name;
|
|
||||||
String? relation;
|
|
||||||
String? gender;
|
|
||||||
DateTime? dateOfBirth;
|
|
||||||
|
|
||||||
FamilyMember({this.name, this.relation, this.gender, this.dateOfBirth});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'name': name,
|
|
||||||
'relation': relation,
|
|
||||||
'gender': gender,
|
|
||||||
'dateOfBirth': dateOfBirth?.toIso8601String(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
FamilyMember.fromJson(Map<String, dynamic> json) {
|
|
||||||
name = json['name'];
|
|
||||||
relation = json['relation'];
|
|
||||||
gender = json['gender'];
|
|
||||||
dateOfBirth = json['dateOfBirth'] != null
|
|
||||||
? DateTime.parse(json['dateOfBirth'])
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatientAddress {
|
|
||||||
String? houseNo;
|
|
||||||
String? line;
|
|
||||||
String? town;
|
|
||||||
String? pincode;
|
|
||||||
String? country;
|
|
||||||
String? state;
|
|
||||||
String? city;
|
|
||||||
String? addressType;
|
|
||||||
String? otherLabel;
|
|
||||||
|
|
||||||
PatientAddress({
|
|
||||||
this.houseNo,
|
|
||||||
this.line,
|
|
||||||
this.town,
|
|
||||||
this.pincode,
|
|
||||||
this.country,
|
|
||||||
this.state,
|
|
||||||
this.city,
|
|
||||||
this.addressType,
|
|
||||||
this.otherLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'houseNo': houseNo,
|
|
||||||
'line': line,
|
|
||||||
'town': town,
|
|
||||||
'pincode': pincode,
|
|
||||||
'country': country,
|
|
||||||
'state': state,
|
|
||||||
'city': city,
|
|
||||||
'addressType': addressType,
|
|
||||||
'otherLabel': otherLabel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
PatientAddress.fromJson(Map<String, dynamic> json) {
|
|
||||||
houseNo = json['houseNo'];
|
|
||||||
line = json['line'];
|
|
||||||
town = json['town'];
|
|
||||||
pincode = json['pincode'];
|
|
||||||
country = json['country'];
|
|
||||||
state = json['state'];
|
|
||||||
city = json['city'];
|
|
||||||
addressType = json['addressType'];
|
|
||||||
otherLabel = json['otherLabel'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:medora/data/models/telemed_user.dart';
|
import 'package:medora/data/models/telemed_user.dart';
|
||||||
import 'package:medora/data/services/data_service.dart';
|
import 'package:medora/data/services/data_service.dart';
|
||||||
import 'package:medora/data/services/doctor_profile_service.dart';
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
import 'package:medora/data/services/patient_registration_service.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class NavigationService {
|
class NavigationService {
|
||||||
@ -17,23 +16,15 @@ class NavigationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (userProfile.role.toLowerCase()) {
|
if (userProfile.role.toLowerCase() != 'doctor') {
|
||||||
case 'doctor':
|
if (context.mounted) {
|
||||||
if (context.mounted) {
|
Navigator.pushReplacementNamed(context, RouteNames.launch);
|
||||||
handleDoctorNavigation(context);
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
if (context.mounted) {
|
||||||
case 'patient':
|
await handleDoctorNavigation(context);
|
||||||
if (context.mounted) {
|
|
||||||
handlePatientNavigation(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pushReplacementNamed(context, RouteNames.launch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error in handleUserNavigation: $e');
|
print('Error in handleUserNavigation: $e');
|
||||||
@ -57,25 +48,25 @@ class NavigationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> handlePatientNavigation(BuildContext context) async {
|
// static Future<void> handlePatientNavigation(BuildContext context) async {
|
||||||
try {
|
// try {
|
||||||
final patientProfile = await PatientProfileService.getPatientProfile();
|
// final patientProfile = await PatientProfileService.getPatientProfile();
|
||||||
|
|
||||||
if (context.mounted) {
|
// if (context.mounted) {
|
||||||
if (patientProfile != null) {
|
// if (patientProfile != null) {
|
||||||
Navigator.pushReplacementNamed(
|
// Navigator.pushReplacementNamed(
|
||||||
context, RouteNames.patientDashboardScreen);
|
// context, RouteNames.patientDashboardScreen);
|
||||||
} else {
|
// } else {
|
||||||
Navigator.pushReplacementNamed(
|
// Navigator.pushReplacementNamed(
|
||||||
context, RouteNames.patientLandingScreen);
|
// context, RouteNames.patientLandingScreen);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
print('Error in handlePatientNavigation: $e');
|
// print('Error in handlePatientNavigation: $e');
|
||||||
if (context.mounted) {
|
// if (context.mounted) {
|
||||||
Navigator.pushReplacementNamed(
|
// Navigator.pushReplacementNamed(
|
||||||
context, RouteNames.patientLandingScreen);
|
// context, RouteNames.patientLandingScreen);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,198 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
||||||
import 'package:medora/controllers/patient_controller.dart';
|
|
||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
class PatientProfileService {
|
|
||||||
static final String patientProfileCollectionName =
|
|
||||||
dotenv.env['PATIENT_PROFILE_COLLECTION_NAME']!;
|
|
||||||
static final FirebaseFirestore db = FirebaseFirestore.instance;
|
|
||||||
static final FirebaseStorage storage = FirebaseStorage.instanceFor(
|
|
||||||
bucket: dotenv.env['FIREBASE_STORAGE_BUCKET']!);
|
|
||||||
|
|
||||||
static Future<String?> uploadProfileImage(File imageFile) async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final String uid = user.uid;
|
|
||||||
final String fileName =
|
|
||||||
'profile_${uid}_${DateTime.now().millisecondsSinceEpoch}${path.extension(imageFile.path)}';
|
|
||||||
final Reference storageRef =
|
|
||||||
storage.ref().child('profile_images/$fileName');
|
|
||||||
final UploadTask uploadTask = storageRef.putFile(
|
|
||||||
imageFile,
|
|
||||||
SettableMetadata(
|
|
||||||
contentType: 'image/${path.extension(imageFile.path).substring(1)}',
|
|
||||||
customMetadata: {
|
|
||||||
'userId': uid,
|
|
||||||
'uploadedAt': DateTime.now().toIso8601String(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final TaskSnapshot snapshot = await uploadTask;
|
|
||||||
final String downloadUrl = await snapshot.ref.getDownloadURL();
|
|
||||||
|
|
||||||
print('Profile image uploaded successfully');
|
|
||||||
return downloadUrl;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error uploading profile image: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> deleteProfileImage(String imageUrl) async {
|
|
||||||
try {
|
|
||||||
final Reference storageRef = storage.refFromURL(imageUrl);
|
|
||||||
await storageRef.delete();
|
|
||||||
print('Profile image deleted successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error deleting profile image: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> savePatientProfile(PatientController controller) async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
final PatientModel patientData = controller.model;
|
|
||||||
String? imageUrl;
|
|
||||||
if (patientData.profileImagePath != null) {
|
|
||||||
final File imageFile = File(patientData.profileImagePath!);
|
|
||||||
imageUrl = await uploadProfileImage(imageFile);
|
|
||||||
if (imageUrl == null) {
|
|
||||||
print('Failed to upload profile image');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, dynamic> patientJson = patientData.toJson();
|
|
||||||
patientJson['createdAt'] = FieldValue.serverTimestamp();
|
|
||||||
patientJson['updatedAt'] = FieldValue.serverTimestamp();
|
|
||||||
patientJson['uid'] = uid;
|
|
||||||
patientJson['profileImageUrl'] = imageUrl;
|
|
||||||
|
|
||||||
await db
|
|
||||||
.collection(patientProfileCollectionName)
|
|
||||||
.doc(uid)
|
|
||||||
.set(patientJson);
|
|
||||||
|
|
||||||
print('Patient profile saved successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error saving patient profile: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> updatePatientProfile(PatientModel patient) async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
String? imageUrl;
|
|
||||||
if (patient.profileImagePath != null) {
|
|
||||||
final DocumentSnapshot oldDoc =
|
|
||||||
await db.collection(patientProfileCollectionName).doc(uid).get();
|
|
||||||
if (oldDoc.exists) {
|
|
||||||
final oldData = oldDoc.data() as Map<String, dynamic>;
|
|
||||||
final String? oldImageUrl = oldData['profileImageUrl'];
|
|
||||||
if (oldImageUrl != null) {
|
|
||||||
await deleteProfileImage(oldImageUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final File imageFile = File(patient.profileImagePath!);
|
|
||||||
imageUrl = await uploadProfileImage(imageFile);
|
|
||||||
if (imageUrl == null) {
|
|
||||||
print('Failed to upload new profile image');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, dynamic> patientJson = patient.toJson();
|
|
||||||
patientJson['updatedAt'] = FieldValue.serverTimestamp();
|
|
||||||
if (imageUrl != null) {
|
|
||||||
patientJson['profileImageUrl'] = imageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.collection(patientProfileCollectionName)
|
|
||||||
.doc(uid)
|
|
||||||
.update(patientJson);
|
|
||||||
|
|
||||||
print('Patient profile updated successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error updating patient profile: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> deletePatientProfile() async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
final DocumentSnapshot doc =
|
|
||||||
await db.collection(patientProfileCollectionName).doc(uid).get();
|
|
||||||
if (doc.exists) {
|
|
||||||
final data = doc.data() as Map<String, dynamic>;
|
|
||||||
final String? imageUrl = data['profileImageUrl'];
|
|
||||||
if (imageUrl != null) {
|
|
||||||
await deleteProfileImage(imageUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.collection(patientProfileCollectionName).doc(uid).delete();
|
|
||||||
|
|
||||||
print('Patient profile deleted successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error deleting patient profile: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<PatientModel?> getPatientProfile() async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final String uid = user.uid;
|
|
||||||
final DocumentSnapshot doc =
|
|
||||||
await db.collection(patientProfileCollectionName).doc(uid).get();
|
|
||||||
if (!doc.exists) {
|
|
||||||
print('No patient profile found for this user');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final data = doc.data() as Map<String, dynamic>;
|
|
||||||
return PatientModel.fromJson(data);
|
|
||||||
} catch (e) {
|
|
||||||
print('Error fetching patient profile: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:telemednet/telemed_user.dart';
|
import 'package:medora/data/models/telemed_user.dart';
|
||||||
|
|
||||||
class DataService {
|
class DataService {
|
||||||
static final String profileCollectionName =
|
static final String profileCollectionName =
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:medora/controllers/consultation_center_controller.dart';
|
import 'package:medora/controllers/consultation_center_controller.dart';
|
||||||
import 'package:medora/data/models/consultation_center.dart';
|
import 'package:medora/data/models/consultation_center.dart';
|
||||||
import 'package:medora/data/models/doctor.dart';
|
import 'package:medora/data/models/doctor.dart';
|
||||||
|
import 'package:medora/data/services/navigation_service.dart';
|
||||||
import 'package:medora/screens/authentication/launch_screen.dart';
|
import 'package:medora/screens/authentication/launch_screen.dart';
|
||||||
// import 'package:medora/data/telemed_user.dart';
|
|
||||||
import 'package:medora/controllers/patient_controller.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
import 'package:medora/screens/authentication/sign_up_screen.dart';
|
||||||
import 'package:medora/screens/common/loading_screen.dart';
|
import 'package:medora/screens/common/loading_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart';
|
||||||
@ -26,41 +26,45 @@ import 'package:medora/screens/doctor_screen/doctor_profile_screens/experience_s
|
|||||||
import 'package:medora/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart';
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/consultation_booking_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/consultation_time_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/consultations_center_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/doctor_details_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/doctors_list_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/patient_dashboard/patient_dashboard_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/patient_dashboard/patient_home_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/patient_dashboard/patient_profile_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/registration_screens/patient_adress_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/registration_screens/patient_family_members_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/registration_screens/patient_registration_screen.dart';
|
|
||||||
import 'package:medora/screens/splash_screen.dart';
|
import 'package:medora/screens/splash_screen.dart';
|
||||||
|
|
||||||
import '../controllers/doctor_controller.dart';
|
import '../controllers/doctor_controller.dart';
|
||||||
import '../screens/patient_screens/patient_landing_screen.dart';
|
|
||||||
import '../screens/patient_screens/registration_screens/family_members_edit_screen.dart';
|
|
||||||
|
|
||||||
final Map<String, Widget Function(BuildContext)> routes = {
|
final Map<String, Widget Function(BuildContext)> routes = {
|
||||||
RouteNames.launch: (context) => const LaunchScreen(),
|
// RouteNames.launch: (context) => const LaunchScreen(),
|
||||||
RouteNames.signIn: (context) => SignInScreen(
|
RouteNames.signIn: (context) => SignInScreen(
|
||||||
providers: [EmailAuthProvider(), PhoneAuthProvider()],
|
providers: [EmailAuthProvider(), PhoneAuthProvider()],
|
||||||
|
showAuthActionSwitch: false,
|
||||||
|
footerBuilder: (context, action) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, RouteNames.signUp);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Don't have an account? Sign up",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
AuthStateChangeAction<SignedIn>((context, state) {
|
||||||
|
print("Sign in successful");
|
||||||
|
NavigationService.handleUserNavigation(context);
|
||||||
|
}),
|
||||||
|
AuthStateChangeAction<AuthFailed>((context, state) {
|
||||||
|
print("Sign in failed: ${state.exception}");
|
||||||
|
}),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
RouteNames.signUp: (context) => const RegisterScreen(),
|
RouteNames.signUp: (context) => const SignUpScreen(),
|
||||||
// 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.userHome: (context) => const UserScreen(),
|
||||||
RouteNames.profileUpload: (context) => const ProfileUploadPage(),
|
|
||||||
RouteNames.patientLandingScreen: (context) => const PatientLandingScreen(),
|
|
||||||
RouteNames.patientHomeScreen: (context) => const PatientHomeScreen(),
|
|
||||||
RouteNames.doctorLandingScreen: (context) => const DoctorLandingScreen(),
|
RouteNames.doctorLandingScreen: (context) => const DoctorLandingScreen(),
|
||||||
RouteNames.patientRegistrationScreen: (context) =>
|
|
||||||
const PatientRegistrationScreen(),
|
|
||||||
RouteNames.qualificationsScreen: (context) {
|
RouteNames.qualificationsScreen: (context) {
|
||||||
final controller =
|
final controller =
|
||||||
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
||||||
@ -68,6 +72,7 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
|||||||
controller: controller ?? DoctorController(), // Provide fallback
|
controller: controller ?? DoctorController(), // Provide fallback
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
RouteNames.profileUpload: (context) => const ProfileUploadPage(),
|
||||||
RouteNames.doctorAddressScreen: (context) {
|
RouteNames.doctorAddressScreen: (context) {
|
||||||
final controller =
|
final controller =
|
||||||
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
ModalRoute.of(context)!.settings.arguments as DoctorController?;
|
||||||
@ -110,65 +115,6 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
|||||||
controller: controller ?? DoctorController(),
|
controller: controller ?? DoctorController(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
RouteNames.patientAdressScreen: (context) {
|
|
||||||
final controller =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as PatientController;
|
|
||||||
return PatientAddressScreen(controller: controller);
|
|
||||||
},
|
|
||||||
RouteNames.patientFamilyMembersScreen: (context) {
|
|
||||||
final controller =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as PatientController;
|
|
||||||
return PatientFamilyMembersScreen(controller: controller);
|
|
||||||
},
|
|
||||||
RouteNames.familyMembersEditScreen: (context) {
|
|
||||||
final controller =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as PatientController;
|
|
||||||
return FamilyMembersEditScreen(controller: controller);
|
|
||||||
},
|
|
||||||
RouteNames.patientprofileScreen: (context) => const PatientProfileScreen(),
|
|
||||||
RouteNames.patientDashboardScreen: (context) =>
|
|
||||||
const PatientDashboardScreen(),
|
|
||||||
RouteNames.specialityScreen: (context) => const SpecialtyScreen(),
|
|
||||||
RouteNames.doctorListScreen: (context) {
|
|
||||||
final args =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
||||||
return DoctorsListScreen(
|
|
||||||
specialty: args['specialty'] as String,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RouteNames.doctorDetailsScreen: (context) {
|
|
||||||
final args =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
||||||
return DoctorDetailsScreen(
|
|
||||||
doctor: args['doctor'] as Doctor,
|
|
||||||
preloadedImage: args['imageProvider'] as ImageProvider?,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RouteNames.consultationCenterScreen: (context) {
|
|
||||||
final args =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
||||||
return ConsultationsCenterScreen(
|
|
||||||
doctor: args['doctor'] as Doctor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RouteNames.consultationTimeScreen: (context) {
|
|
||||||
final args =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
||||||
return ConsultationTimeScreen(
|
|
||||||
doctor: args['doctor'] as Doctor,
|
|
||||||
selectedConsultation: args['selectedConsultation'] as ConsultationCenter,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RouteNames.consultationBookingScreen: (context) {
|
|
||||||
final args =
|
|
||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
|
||||||
return ConsultationBookingScreen(
|
|
||||||
doctor: args['doctor'] as Doctor,
|
|
||||||
selectedConsultation: args['selectedConsultation'] as ConsultationCenter,
|
|
||||||
selectedDate: args['selectedDate'] as DateTime,
|
|
||||||
selectedTime: args['selectedTime'] as String,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
RouteNames.doctorDashbordScreen: (context) => const DoctorDashboardScreen(),
|
RouteNames.doctorDashbordScreen: (context) => const DoctorDashboardScreen(),
|
||||||
RouteNames.doctorHomeScreen: (context) => const DoctorDashboardHomeScreen(),
|
RouteNames.doctorHomeScreen: (context) => const DoctorDashboardHomeScreen(),
|
||||||
RouteNames.doctorPersonalProfileScreen: (context) =>
|
RouteNames.doctorPersonalProfileScreen: (context) =>
|
||||||
|
|||||||
@ -185,9 +185,7 @@ class _LaunchScreenState extends State<LaunchScreen> {
|
|||||||
void _navigateToSignUp() {
|
void _navigateToSignUp() {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SignUpScreen(
|
builder: (context) => const SignUpScreen(),
|
||||||
selectedUserType: selectedUserType!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import 'package:medora/data/services/navigation_service.dart';
|
|||||||
import 'package:medora/widgets/primary_button.dart';
|
import 'package:medora/widgets/primary_button.dart';
|
||||||
|
|
||||||
class SignUpScreen extends StatefulWidget {
|
class SignUpScreen extends StatefulWidget {
|
||||||
final String selectedUserType;
|
// final String selectedUserType;
|
||||||
|
|
||||||
const SignUpScreen({
|
const SignUpScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.selectedUserType,
|
// required this.selectedUserType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -160,7 +160,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Register as ${widget.selectedUserType}',
|
'Register as doctor',
|
||||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@ -299,17 +299,13 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
final result = await DataService.createUserProfile(
|
final result = await DataService.createUserProfile(
|
||||||
email: _emailController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
password: _passwordController.text,
|
password: _passwordController.text,
|
||||||
userType: widget.selectedUserType,
|
userType: 'doctor',
|
||||||
phoneNumber: _completePhoneNumber,
|
phoneNumber: _completePhoneNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (result['success']) {
|
if (result['success']) {
|
||||||
if (widget.selectedUserType.toLowerCase() == 'doctor') {
|
await NavigationService.handleDoctorNavigation(context);
|
||||||
await NavigationService.handleDoctorNavigation(context);
|
|
||||||
} else {
|
|
||||||
await NavigationService.handlePatientNavigation(context);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_showErrorSnackBar(result['message']);
|
_showErrorSnackBar(result['message']);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,7 +187,7 @@ class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
|||||||
try {
|
try {
|
||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pushReplacementNamed(RouteNames.launch);
|
Navigator.of(context).pushReplacementNamed(RouteNames.signIn);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error signing out: $e");
|
print("Error signing out: $e");
|
||||||
|
|||||||
@ -1,842 +0,0 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:medora/data/models/consultation_center.dart';
|
|
||||||
import 'package:medora/data/models/doctor.dart';
|
|
||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
import 'package:medora/data/services/consultation_booking_service.dart';
|
|
||||||
import 'package:medora/data/services/patient_registration_service.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:medora/widgets/alert_screen.dart';
|
|
||||||
|
|
||||||
class ConsultationBookingScreen extends StatefulWidget {
|
|
||||||
final Doctor doctor;
|
|
||||||
final ConsultationCenter selectedConsultation;
|
|
||||||
final DateTime selectedDate;
|
|
||||||
final String selectedTime;
|
|
||||||
|
|
||||||
const ConsultationBookingScreen({
|
|
||||||
super.key,
|
|
||||||
required this.doctor,
|
|
||||||
required this.selectedConsultation,
|
|
||||||
required this.selectedDate,
|
|
||||||
required this.selectedTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsultationBookingScreen> createState() =>
|
|
||||||
_ConsultationBookingScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsultationBookingScreenState extends State<ConsultationBookingScreen> {
|
|
||||||
PatientModel? selectedPatient;
|
|
||||||
List<PatientModel> familyMembers = [];
|
|
||||||
FamilyMember? selectedFamilyMember;
|
|
||||||
bool isLoading = true;
|
|
||||||
final TextEditingController _nameController = TextEditingController();
|
|
||||||
final TextEditingController _relationController = TextEditingController();
|
|
||||||
DateTime? _selectedDateOfBirth;
|
|
||||||
String _selectedGender = 'Male';
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_relationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadPatientProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadPatientProfile() async {
|
|
||||||
setState(() => isLoading = true);
|
|
||||||
try {
|
|
||||||
final currentPatient = await PatientProfileService.getPatientProfile();
|
|
||||||
if (currentPatient != null) {
|
|
||||||
setState(() {
|
|
||||||
selectedPatient = currentPatient;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error loading patient data: $e');
|
|
||||||
} finally {
|
|
||||||
setState(() => isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get formattedAddress {
|
|
||||||
final parts = [
|
|
||||||
widget.selectedConsultation.floorBuilding,
|
|
||||||
widget.selectedConsultation.street,
|
|
||||||
widget.selectedConsultation.city,
|
|
||||||
widget.selectedConsultation.state,
|
|
||||||
widget.selectedConsultation.postalCode
|
|
||||||
].where((part) => part != null && part.isNotEmpty).toList();
|
|
||||||
return parts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
appBar: _buildAppBar(context),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildAppointmentCard(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildDoctorDetails(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildLocationDetails(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildPaymentDetails(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildInClinicAppointmentText(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildConfirmButton(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInClinicAppointmentText() {
|
|
||||||
String patientName =
|
|
||||||
selectedFamilyMember?.name ?? selectedPatient?.name ?? 'Select Patient';
|
|
||||||
String relation = selectedFamilyMember?.relation != null
|
|
||||||
? ' (${selectedFamilyMember!.relation})'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
const TextSpan(text: 'In-clinic appointment for '),
|
|
||||||
TextSpan(
|
|
||||||
text: '$patientName$relation',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _showPatientSelectionDialog,
|
|
||||||
child: Text(
|
|
||||||
'Change',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
|
||||||
return AppBar(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Booking Overview',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAppointmentCard() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.withOpacity(0.3),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.calendar_today, color: Colors.white),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
DateFormat('EEEE, MMMM d').format(widget.selectedDate),
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.access_time, color: Colors.white),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
widget.selectedTime,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorDetails() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
widget.doctor.profileImageUrl!,
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.doctor.firstName ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.doctor.speciality!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${widget.doctor.yearsOfExperience} years experience',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLocationDetails() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Location',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
formattedAddress,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes} minutes',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPaymentDetails() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Payment Details',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Consultation Fee',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'₹${widget.selectedConsultation.consultationFee ?? "500"}',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConfirmButton(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Handle payment and booking confirmation
|
|
||||||
_showConfirmationDialog(context);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Confirm & Pay',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showConfirmationDialog(BuildContext context) async {
|
|
||||||
if (selectedPatient == null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Please select a patient for the appointment'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bookingService = BookingService();
|
|
||||||
final currentUser = FirebaseAuth.instance.currentUser;
|
|
||||||
|
|
||||||
// Get the correct patient name based on selection
|
|
||||||
final patientName = selectedFamilyMember != null
|
|
||||||
? selectedFamilyMember!.name
|
|
||||||
: selectedPatient!.name;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (context.mounted) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (context) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final bookingId = await bookingService.createBooking(
|
|
||||||
doctorId: widget.doctor.uid!,
|
|
||||||
profileImageUrl: widget.doctor.profileImageUrl!,
|
|
||||||
doctorName: widget.doctor.firstName ?? 'Doctor',
|
|
||||||
patientId: currentUser!.uid,
|
|
||||||
patientName: patientName ?? 'Patient',
|
|
||||||
location: formattedAddress,
|
|
||||||
appointmentDate: widget.selectedDate,
|
|
||||||
appointmentTime: widget.selectedTime,
|
|
||||||
consultationFee:
|
|
||||||
int.parse(widget.selectedConsultation.consultationFee ?? "500"),
|
|
||||||
specialization: widget.doctor.speciality!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => AlertScreen(
|
|
||||||
arguments: AlertArguments(
|
|
||||||
title: 'Booking Confirmed',
|
|
||||||
message:
|
|
||||||
'Your in-clinic appointment has been successfully booked for $patientName. Booking ID: ${bookingId.substring(0, 8)}\n\nPlease complete the payment to confirm your appointment.',
|
|
||||||
actionTitle: 'View Appointments',
|
|
||||||
type: AlertType.success,
|
|
||||||
onActionPressed: () {
|
|
||||||
Navigator.pushReplacementNamed(
|
|
||||||
context, RouteNames.patientDashboardScreen);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => AlertScreen(
|
|
||||||
arguments: AlertArguments(
|
|
||||||
title: 'Booking Failed',
|
|
||||||
message: 'Unable to create booking. ${e.toString()}',
|
|
||||||
actionTitle: 'Try Again',
|
|
||||||
type: AlertType.error,
|
|
||||||
onActionPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showAddFamilyMemberDialog() async {
|
|
||||||
_nameController.clear();
|
|
||||||
_relationController.clear();
|
|
||||||
setState(() {
|
|
||||||
_selectedDateOfBirth = null;
|
|
||||||
_selectedGender = 'Male';
|
|
||||||
});
|
|
||||||
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext dialogContext) => StatefulBuilder(
|
|
||||||
builder: (BuildContext context, StateSetter setDialogState) {
|
|
||||||
return AlertDialog(
|
|
||||||
shape:
|
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
title: Text(
|
|
||||||
'Add Family Member',
|
|
||||||
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
content: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: _nameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Full Name',
|
|
||||||
labelStyle: GoogleFonts.poppins(),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(
|
|
||||||
Icons.person_outline,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: _relationController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Relation',
|
|
||||||
labelStyle: GoogleFonts.poppins(),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(
|
|
||||||
Icons.family_restroom,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
InkWell(
|
|
||||||
onTap: () async {
|
|
||||||
final DateTime? picked = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: DateTime.now(),
|
|
||||||
firstDate: DateTime(1900),
|
|
||||||
lastDate: DateTime.now(),
|
|
||||||
builder: (context, child) {
|
|
||||||
return Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
colorScheme: ColorScheme.light(
|
|
||||||
primary: Colors.blue,
|
|
||||||
onPrimary: Colors.white,
|
|
||||||
surface: Colors.grey[100]!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (picked != null) {
|
|
||||||
setDialogState(() {
|
|
||||||
_selectedDateOfBirth = picked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.calendar_today,
|
|
||||||
color: Colors.blue),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
_selectedDateOfBirth != null
|
|
||||||
? DateFormat('dd/MM/yyyy')
|
|
||||||
.format(_selectedDateOfBirth!)
|
|
||||||
: 'Select Date of Birth',
|
|
||||||
style: GoogleFonts.poppins(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
value: _selectedGender,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: const Icon(Icons.person_outline,
|
|
||||||
color: Colors.blue),
|
|
||||||
border: InputBorder.none,
|
|
||||||
labelStyle: GoogleFonts.poppins(),
|
|
||||||
),
|
|
||||||
items: ['Male', 'Female', 'Other']
|
|
||||||
.map((gender) => DropdownMenuItem(
|
|
||||||
value: gender,
|
|
||||||
child: Text(gender,
|
|
||||||
style: GoogleFonts.poppins()),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
setDialogState(() => _selectedGender = value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(
|
|
||||||
'Cancel',
|
|
||||||
style: GoogleFonts.poppins(color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => _addFamilyMember(context),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Add Member',
|
|
||||||
style: GoogleFonts.poppins(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _addFamilyMember(BuildContext context) async {
|
|
||||||
if (_nameController.text.isEmpty ||
|
|
||||||
_relationController.text.isEmpty ||
|
|
||||||
_selectedDateOfBirth == null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Please fill in all fields',
|
|
||||||
style: GoogleFonts.poppins(),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final newFamilyMember = FamilyMember(
|
|
||||||
name: _nameController.text,
|
|
||||||
relation: _relationController.text,
|
|
||||||
gender: _selectedGender,
|
|
||||||
dateOfBirth: _selectedDateOfBirth,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedPatient != null) {
|
|
||||||
selectedPatient!.familyMembers.add(newFamilyMember);
|
|
||||||
await PatientProfileService.updatePatientProfile(selectedPatient!);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
selectedFamilyMember = newFamilyMember;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
_showPatientSelectionDialog();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context); // Pop add family member dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPatientSelectionDialog() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
title: Text(
|
|
||||||
'Select Patient',
|
|
||||||
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.maxFinite,
|
|
||||||
child: ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
// Main patient
|
|
||||||
_buildPatientTile(
|
|
||||||
name: selectedPatient?.name ?? '',
|
|
||||||
subtitle: 'Primary Patient',
|
|
||||||
isSelected: selectedFamilyMember == null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
selectedFamilyMember = null;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
// Family members
|
|
||||||
...selectedPatient?.familyMembers.map(
|
|
||||||
(member) => _buildPatientTile(
|
|
||||||
name: member.name ?? '',
|
|
||||||
subtitle: member.relation ?? '',
|
|
||||||
isSelected: selectedFamilyMember == member,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
selectedFamilyMember = member;
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
[],
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.person_add, color: Colors.blue),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Add Family Member',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showAddFamilyMemberDialog();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPatientTile({
|
|
||||||
required String name,
|
|
||||||
required String subtitle,
|
|
||||||
required bool isSelected,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? Colors.blue.withOpacity(0.1)
|
|
||||||
: Colors.grey.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
color: isSelected ? Colors.blue : Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
name,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
|
||||||
color: isSelected ? Colors.blue : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
subtitle,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: isSelected
|
|
||||||
? const Icon(Icons.check_circle, color: Colors.blue)
|
|
||||||
: null,
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,553 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:medora/data/models/consultation_center.dart';
|
|
||||||
import 'package:medora/data/models/doctor.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
|
|
||||||
class ConsultationTimeScreen extends StatefulWidget {
|
|
||||||
final Doctor doctor;
|
|
||||||
final ConsultationCenter selectedConsultation;
|
|
||||||
|
|
||||||
const ConsultationTimeScreen({
|
|
||||||
super.key,
|
|
||||||
required this.doctor,
|
|
||||||
required this.selectedConsultation,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsultationTimeScreen> createState() => _ConsultationTimeScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsultationTimeScreenState extends State<ConsultationTimeScreen> {
|
|
||||||
DateTime? selectedDate;
|
|
||||||
String? selectedTime;
|
|
||||||
|
|
||||||
List<TimeSlot> getTimeSlotsForDay(String dayName) {
|
|
||||||
try {
|
|
||||||
final schedule = widget.selectedConsultation.weeklySchedule?.firstWhere(
|
|
||||||
(schedule) => schedule.day == dayName,
|
|
||||||
orElse: () => AvailabilitySchedule(
|
|
||||||
day: dayName,
|
|
||||||
timeSlots: [],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return schedule?.timeSlots ?? [];
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error getting time slots: $e');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? parseTimeString(String? timeStr) {
|
|
||||||
if (timeStr == null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try parsing 12-hour format first
|
|
||||||
return DateFormat('h:mm a').parse(timeStr);
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
// Try parsing 24-hour format
|
|
||||||
return DateFormat('HH:mm').parse(timeStr);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error parsing time: $timeStr');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get formattedAddress {
|
|
||||||
final parts = [
|
|
||||||
widget.selectedConsultation.floorBuilding,
|
|
||||||
widget.selectedConsultation.street,
|
|
||||||
widget.selectedConsultation.city,
|
|
||||||
widget.selectedConsultation.state,
|
|
||||||
widget.selectedConsultation.postalCode
|
|
||||||
].where((part) => part != null && part.isNotEmpty).toList();
|
|
||||||
return parts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
appBar: _buildAppBar(),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildLocationInfo(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildDateSelection(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildTimeSlots(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar() {
|
|
||||||
return AppBar(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Select Date & Time',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLocationInfo() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.doctor.firstName ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.doctor.speciality!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
widget.doctor.profileImageUrl!,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
child:
|
|
||||||
Icon(Icons.person, size: 30, color: Colors.grey[600]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(height: 24),
|
|
||||||
Text(
|
|
||||||
'Selected Location',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
widget.selectedConsultation.city ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes}',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDateSelection() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Select Date',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SizedBox(
|
|
||||||
height: 100,
|
|
||||||
child: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: 20, // Show next 20 days
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final date = DateTime.now().add(Duration(days: index));
|
|
||||||
final isSelected = selectedDate?.day == date.day &&
|
|
||||||
selectedDate?.month == date.month &&
|
|
||||||
selectedDate?.year == date.year;
|
|
||||||
final isAvailable = _isDateAvailable(date);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: isAvailable
|
|
||||||
? () {
|
|
||||||
setState(() {
|
|
||||||
selectedDate = date;
|
|
||||||
selectedTime = null; // Reset time when date changes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Container(
|
|
||||||
width: 70,
|
|
||||||
margin: const EdgeInsets.only(right: 12),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? Colors.blue
|
|
||||||
: isAvailable
|
|
||||||
? Colors.white
|
|
||||||
: Colors.grey[200],
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: isAvailable
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
DateFormat('EEE').format(date).toUpperCase(),
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isSelected
|
|
||||||
? Colors.white
|
|
||||||
: isAvailable
|
|
||||||
? Colors.grey[600]
|
|
||||||
: Colors.grey[400],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
date.day.toString(),
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isSelected
|
|
||||||
? Colors.white
|
|
||||||
: isAvailable
|
|
||||||
? Colors.black87
|
|
||||||
: Colors.grey[400],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTimeSlots() {
|
|
||||||
if (selectedDate == null) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.5),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'Please select a date to view available time slots',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final dayName = DateFormat('EEEE').format(selectedDate!);
|
|
||||||
final timeSlots = getTimeSlotsForDay(dayName);
|
|
||||||
final allTimeSlots = _generateTimeSlots(timeSlots);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Select Time',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: allTimeSlots.map((time) {
|
|
||||||
final isSelected = selectedTime == time;
|
|
||||||
final isAvailable = _isTimeSlotAvailable(time);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: isAvailable
|
|
||||||
? () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.consultationBookingScreen,
|
|
||||||
arguments: {
|
|
||||||
'doctor': widget.doctor,
|
|
||||||
'selectedConsultation': widget.selectedConsultation,
|
|
||||||
'selectedDate': selectedDate,
|
|
||||||
'selectedTime': time
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? Colors.blue
|
|
||||||
: isAvailable
|
|
||||||
? Colors.white
|
|
||||||
: Colors.grey[200],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected
|
|
||||||
? Colors.blue
|
|
||||||
: isAvailable
|
|
||||||
? Colors.grey.withOpacity(0.2)
|
|
||||||
: Colors.grey.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
time,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: isSelected
|
|
||||||
? Colors.white
|
|
||||||
: isAvailable
|
|
||||||
? Colors.black87
|
|
||||||
: Colors.grey[400],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isDateAvailable(DateTime date) {
|
|
||||||
final dayName = DateFormat('EEEE').format(date);
|
|
||||||
return widget.selectedConsultation.weeklySchedule
|
|
||||||
?.any((schedule) => schedule.day == dayName) ??
|
|
||||||
false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _generateTimeSlots(List<TimeSlot> timeSlots) {
|
|
||||||
final slots = <String>[];
|
|
||||||
final timeFormat = DateFormat('h:mm a');
|
|
||||||
|
|
||||||
for (var slot in timeSlots) {
|
|
||||||
final startTime = parseTimeString(slot.startTime);
|
|
||||||
final endTime = parseTimeString(slot.endTime);
|
|
||||||
|
|
||||||
if (startTime == null || endTime == null) continue;
|
|
||||||
|
|
||||||
var currentTime = startTime;
|
|
||||||
while (currentTime.isBefore(endTime)) {
|
|
||||||
slots.add(timeFormat.format(currentTime));
|
|
||||||
currentTime = currentTime.add(const Duration(minutes: 30));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return slots;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isTimeSlotAvailable(String time) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
if (selectedDate == null) return false;
|
|
||||||
|
|
||||||
// Parse the time slot
|
|
||||||
final timeSlot = parseTimeString(time);
|
|
||||||
if (timeSlot == null) return false;
|
|
||||||
|
|
||||||
// Create a DateTime combining selected date and time
|
|
||||||
final slotDateTime = DateTime(
|
|
||||||
selectedDate!.year,
|
|
||||||
selectedDate!.month,
|
|
||||||
selectedDate!.day,
|
|
||||||
timeSlot.hour,
|
|
||||||
timeSlot.minute,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if the slot is in the past
|
|
||||||
if (slotDateTime.isBefore(now)) return false;
|
|
||||||
|
|
||||||
// Here you would typically check against your booking database
|
|
||||||
// For now, returning true for future slots
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBooking() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Confirm Booking',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildConfirmationDetail(
|
|
||||||
'Doctor',
|
|
||||||
widget.doctor.firstName!,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildConfirmationDetail(
|
|
||||||
'Location',
|
|
||||||
widget.selectedConsultation.city!,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildConfirmationDetail(
|
|
||||||
'Date',
|
|
||||||
DateFormat('EEEE, MMMM d').format(selectedDate!),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildConfirmationDetail(
|
|
||||||
'Time',
|
|
||||||
selectedTime!,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(
|
|
||||||
'Cancel',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Confirm',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConfirmationDetail(String label, String value) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 80,
|
|
||||||
child: Text(
|
|
||||||
'$label:',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,312 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:medora/data/models/doctor.dart';
|
|
||||||
import 'package:medora/data/models/consultation_center.dart';
|
|
||||||
import 'package:medora/data/services/consultation_center_service.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
|
|
||||||
class ConsultationsCenterScreen extends StatefulWidget {
|
|
||||||
final Doctor doctor;
|
|
||||||
|
|
||||||
const ConsultationsCenterScreen({
|
|
||||||
super.key,
|
|
||||||
required this.doctor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConsultationsCenterScreen> createState() =>
|
|
||||||
_ConsultationsCenterScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConsultationsCenterScreenState extends State<ConsultationsCenterScreen> {
|
|
||||||
List<ConsultationCenter> _consultationCenters = [];
|
|
||||||
bool _isLoading = true;
|
|
||||||
String? _error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_fetchDoctorConsultationCenters();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchDoctorConsultationCenters() async {
|
|
||||||
try {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (widget.doctor.uid == null) {
|
|
||||||
throw Exception('Doctor UID is missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
final centers =
|
|
||||||
await ConsultationCenterService.getDoctorConsultationCenters(
|
|
||||||
widget.doctor.uid!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_consultationCenters = centers;
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_error = e.toString();
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Failed to load consultation centers: $e')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatAddress(ConsultationCenter center) {
|
|
||||||
List<String> addressParts = [];
|
|
||||||
|
|
||||||
if (center.floorBuilding != null && center.floorBuilding!.isNotEmpty) {
|
|
||||||
addressParts.add(center.floorBuilding!);
|
|
||||||
}
|
|
||||||
if (center.street != null && center.street!.isNotEmpty) {
|
|
||||||
addressParts.add(center.street!);
|
|
||||||
}
|
|
||||||
if (center.city != null && center.city!.isNotEmpty) {
|
|
||||||
addressParts.add(center.city!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return addressParts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
appBar: _buildAppBar(),
|
|
||||||
body: RefreshIndicator(
|
|
||||||
onRefresh: _fetchDoctorConsultationCenters,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildDoctorInfo(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildConsultationLocations(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar() {
|
|
||||||
return AppBar(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Select Location',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorInfo() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
widget.doctor.profileImageUrl!,
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
child: Icon(Icons.person, size: 40, color: Colors.grey[600]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.doctor.firstName ?? "",
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.doctor.speciality!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${widget.doctor.yearsOfExperience} years experience',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConsultationLocations() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Select Location',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
if (_isLoading)
|
|
||||||
const Center(child: CircularProgressIndicator())
|
|
||||||
else if (_error != null)
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Error loading centers',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _fetchDoctorConsultationCenters,
|
|
||||||
child: const Text('Retry'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (_consultationCenters.isEmpty)
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
'No consultation centers available',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SizedBox(
|
|
||||||
height: 120,
|
|
||||||
child: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: _consultationCenters.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final center = _consultationCenters[index];
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.consultationTimeScreen,
|
|
||||||
arguments: {
|
|
||||||
'doctor': widget.doctor,
|
|
||||||
'selectedConsultation': center,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
width: 200,
|
|
||||||
margin: const EdgeInsets.only(right: 16),
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_formatAddress(center),
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (center.averageDurationMinutes != null)
|
|
||||||
Text(
|
|
||||||
'Average time: ${center.averageDurationMinutes} mins',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (center.consultationFee != null)
|
|
||||||
Text(
|
|
||||||
'Fee: ${center.consultationFee}',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,346 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:medora/data/models/doctor.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
|
|
||||||
class DoctorDetailsScreen extends StatefulWidget {
|
|
||||||
final Doctor doctor;
|
|
||||||
final ImageProvider? preloadedImage;
|
|
||||||
|
|
||||||
const DoctorDetailsScreen({
|
|
||||||
super.key,
|
|
||||||
required this.doctor,
|
|
||||||
this.preloadedImage,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DoctorDetailsScreen> createState() => _DoctorDetailsScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DoctorDetailsScreenState extends State<DoctorDetailsScreen> {
|
|
||||||
bool isDescriptionExpanded = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
_buildAppBar(context),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildDoctorCard(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildDescription(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildQualifications(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context, RouteNames.consultationCenterScreen,
|
|
||||||
arguments: {
|
|
||||||
'doctor': widget.doctor,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
disabledBackgroundColor: Colors.grey[300],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Confirm Booking',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAppBar(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.white,
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Doctor',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorCard() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildDoctorImage(),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.doctor.firstName ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.doctor.speciality!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.medical_services,
|
|
||||||
size: 16, color: Colors.blue[400]),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.doctor.speciality!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.location_on,
|
|
||||||
size: 16, color: Colors.blue[400]),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
widget.doctor.city!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.star, size: 16, color: Colors.blue[400]),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${widget.doctor.yearsOfExperience} Years',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorImage() {
|
|
||||||
final imageProvider =
|
|
||||||
widget.preloadedImage ?? NetworkImage(widget.doctor.profileImageUrl!);
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image(
|
|
||||||
image: imageProvider,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
|
||||||
if (wasSynchronouslyLoaded || frame != null) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 50,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDescription() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Description',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
widget.doctor.profileDescription!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
maxLines: isDescriptionExpanded ? null : 3,
|
|
||||||
overflow: isDescriptionExpanded ? null : TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
isDescriptionExpanded = !isDescriptionExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
minimumSize: Size.zero,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
isDescriptionExpanded ? 'Show less' : 'Read more',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQualifications() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Qualifications',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
widget.doctor.qualifications?.join(', ') ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,387 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:medora/data/models/doctor.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
|
|
||||||
class DoctorsListScreen extends StatefulWidget {
|
|
||||||
final String specialty;
|
|
||||||
|
|
||||||
const DoctorsListScreen({
|
|
||||||
super.key,
|
|
||||||
required this.specialty,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DoctorsListScreen> createState() => _DoctorsListScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|
||||||
late final Query doctorsQuery;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
bool _isSearching = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
doctorsQuery = FirebaseFirestore.instance
|
|
||||||
.collection('doctorprofiles')
|
|
||||||
.where('speciality', isEqualTo: widget.specialty);
|
|
||||||
_searchController.addListener(_onSearchChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_searchController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSearchChanged() {
|
|
||||||
setState(() {
|
|
||||||
_isSearching = _searchController.text.isNotEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
_buildSliverAppBar(),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _buildSearchBar(),
|
|
||||||
),
|
|
||||||
_buildDoctorsList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSliverAppBar() {
|
|
||||||
return SliverAppBar(
|
|
||||||
expandedHeight: 55,
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
stretch: true,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
title: Text(
|
|
||||||
'${widget.specialty} Specialists',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSearchBar() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search doctors...',
|
|
||||||
hintStyle: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
|
||||||
suffixIcon: _isSearching
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
_searchController.clear();
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorsList() {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
sliver: StreamBuilder<QuerySnapshot>(
|
|
||||||
stream: doctorsQuery.snapshots(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) => _buildShimmerDoctorCard(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
|
|
||||||
return SliverFillRemaining(
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.medical_services_outlined,
|
|
||||||
size: 64,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'No doctors available in this specialty',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final doctors = snapshot.data!.docs
|
|
||||||
.map((doc) => Doctor.fromJson(doc.data() as Map<String, dynamic>))
|
|
||||||
.where((doctor) {
|
|
||||||
if (_searchController.text.isEmpty) return true;
|
|
||||||
final searchQuery = _searchController.text.toLowerCase();
|
|
||||||
return doctor.firstName!.toLowerCase().contains(searchQuery) ||
|
|
||||||
doctor.city!.toLowerCase().contains(searchQuery);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
final doctor = doctors[index];
|
|
||||||
return _buildDoctorCard(doctor);
|
|
||||||
},
|
|
||||||
childCount: doctors.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildShimmerDoctorCard() {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 120,
|
|
||||||
height: 20,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
width: 150,
|
|
||||||
height: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
width: 100,
|
|
||||||
height: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorCard(Doctor doctor) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
precacheImage(NetworkImage(doctor.profileImageUrl!), context);
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.doctorDetailsScreen,
|
|
||||||
arguments: {
|
|
||||||
'doctor': doctor,
|
|
||||||
'imageProvider': NetworkImage(doctor.profileImageUrl!),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildDoctorImage(doctor),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
doctor.firstName ?? '',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'${doctor.yearsOfExperience!} years experience',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.location_on,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
doctor.city!,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDoctorImage(Doctor doctor) {
|
|
||||||
return Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
doctor.profileImageUrl!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey[200],
|
|
||||||
child: Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 40,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,426 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:animate_do/animate_do.dart';
|
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
|
|
||||||
class Specialty {
|
|
||||||
final String name;
|
|
||||||
final IconData icon;
|
|
||||||
final Color color;
|
|
||||||
final String description;
|
|
||||||
|
|
||||||
Specialty({
|
|
||||||
required this.name,
|
|
||||||
required this.icon,
|
|
||||||
required this.color,
|
|
||||||
required this.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpecialtyScreen extends StatefulWidget {
|
|
||||||
const SpecialtyScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SpecialtyScreen> createState() => _SpecialtyScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpecialtyScreenState extends State<SpecialtyScreen> {
|
|
||||||
final List<Specialty> _allSpecialties = [
|
|
||||||
Specialty(
|
|
||||||
name: 'Pediatric',
|
|
||||||
icon: Icons.child_care,
|
|
||||||
color: Colors.blue,
|
|
||||||
description: 'Medical care for infants, children, and adolescents',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'General Medicine',
|
|
||||||
icon: Icons.medical_services,
|
|
||||||
color: Colors.green,
|
|
||||||
description:
|
|
||||||
'Primary healthcare for adults and general medical conditions',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Family Medicine',
|
|
||||||
icon: Icons.family_restroom,
|
|
||||||
color: Colors.teal,
|
|
||||||
description: 'Comprehensive healthcare for families and individuals',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Cardiologist',
|
|
||||||
icon: Icons.favorite,
|
|
||||||
color: Colors.red,
|
|
||||||
description: 'Diagnosis and treatment of heart conditions',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Neurology',
|
|
||||||
icon: Icons.psychology,
|
|
||||||
color: Colors.purple,
|
|
||||||
description: 'Treatment of nervous system disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Gastroenterology',
|
|
||||||
icon: Icons.local_hospital,
|
|
||||||
color: Colors.orange,
|
|
||||||
description: 'Digestive system disorders and treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Dermatologist',
|
|
||||||
icon: Icons.face,
|
|
||||||
color: Colors.pink,
|
|
||||||
description: 'Skin, hair, and nail conditions',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Orthopedic',
|
|
||||||
icon: Icons.wheelchair_pickup,
|
|
||||||
color: Colors.indigo,
|
|
||||||
description: 'Musculoskeletal system and injury treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Ophthalmology',
|
|
||||||
icon: Icons.remove_red_eye,
|
|
||||||
color: Colors.brown,
|
|
||||||
description: 'Eye care and vision treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'ENT',
|
|
||||||
icon: Icons.hearing,
|
|
||||||
color: Colors.cyan,
|
|
||||||
description: 'Ear, nose, and throat specialist',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Psychiatry',
|
|
||||||
icon: Icons.psychology_outlined,
|
|
||||||
color: Colors.deepPurple,
|
|
||||||
description: 'Mental health and behavioral disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Gynecology',
|
|
||||||
icon: Icons.pregnant_woman,
|
|
||||||
color: Colors.pinkAccent,
|
|
||||||
description: "Women's health and reproductive care",
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Urology',
|
|
||||||
icon: Icons.water_drop,
|
|
||||||
color: Colors.lightBlue,
|
|
||||||
description: 'Urinary tract and male reproductive health',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Endocrinology',
|
|
||||||
icon: Icons.biotech,
|
|
||||||
color: Colors.amber,
|
|
||||||
description: 'Hormone and metabolic disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Oncology',
|
|
||||||
icon: Icons.bloodtype,
|
|
||||||
color: Colors.redAccent,
|
|
||||||
description: 'Cancer diagnosis and treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Rheumatology',
|
|
||||||
icon: Icons.accessibility,
|
|
||||||
color: Colors.deepOrange,
|
|
||||||
description: 'Arthritis and autoimmune conditions',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Pulmonology',
|
|
||||||
icon: Icons.air,
|
|
||||||
color: Colors.lightGreen,
|
|
||||||
description: 'Respiratory system disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Nephrology',
|
|
||||||
icon: Icons.water,
|
|
||||||
color: Colors.blueGrey,
|
|
||||||
description: 'Kidney diseases and disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Dentistry',
|
|
||||||
icon: Icons.cleaning_services,
|
|
||||||
color: Colors.cyan,
|
|
||||||
description: 'Oral health and dental care',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Physical Therapy',
|
|
||||||
icon: Icons.accessibility_new,
|
|
||||||
color: Colors.deepPurple,
|
|
||||||
description: 'Rehabilitation and physical medicine',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Sports Medicine',
|
|
||||||
icon: Icons.sports,
|
|
||||||
color: Colors.green,
|
|
||||||
description: 'Athletic injuries and performance',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Allergy & Immunology',
|
|
||||||
icon: Icons.sick,
|
|
||||||
color: Colors.orange,
|
|
||||||
description: 'Allergies and immune system disorders',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Pain Management',
|
|
||||||
icon: Icons.healing,
|
|
||||||
color: Colors.red,
|
|
||||||
description: 'Chronic pain treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Sleep Medicine',
|
|
||||||
icon: Icons.bedtime,
|
|
||||||
color: Colors.indigo,
|
|
||||||
description: 'Sleep disorders and treatment',
|
|
||||||
),
|
|
||||||
Specialty(
|
|
||||||
name: 'Geriatrics',
|
|
||||||
icon: Icons.elderly,
|
|
||||||
color: Colors.brown,
|
|
||||||
description: 'Healthcare for elderly patients',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
late List<Specialty> _filteredSpecialties;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
bool _isSearching = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_filteredSpecialties = _allSpecialties;
|
|
||||||
_searchController.addListener(_onSearchChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_searchController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSearchChanged() {
|
|
||||||
final searchQuery = _searchController.text.toLowerCase();
|
|
||||||
setState(() {
|
|
||||||
_isSearching = searchQuery.isNotEmpty;
|
|
||||||
_filteredSpecialties = _allSpecialties
|
|
||||||
.where((specialty) =>
|
|
||||||
specialty.name.toLowerCase().contains(searchQuery) ||
|
|
||||||
specialty.description.toLowerCase().contains(searchQuery))
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFFF5F7FF),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
_buildSliverAppBar(),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _buildSearchBar(),
|
|
||||||
),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
sliver: _buildSpecialtiesGrid(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSliverAppBar() {
|
|
||||||
return SliverAppBar(
|
|
||||||
expandedHeight: 55,
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
stretch: true,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
title: Text(
|
|
||||||
'Find a Specialist',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSearchBar() {
|
|
||||||
return FadeIn(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search specialties...',
|
|
||||||
hintStyle: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
|
||||||
suffixIcon: _isSearching
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
_searchController.clear();
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSpecialtiesGrid() {
|
|
||||||
return SliverAnimationBuilder(
|
|
||||||
child: MasonryGridView.count(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: _filteredSpecialties.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final specialty = _filteredSpecialties[index];
|
|
||||||
return FadeInUp(
|
|
||||||
delay: Duration(milliseconds: 100 * index),
|
|
||||||
child: _buildSpecialtyCard(specialty),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSpecialtyCard(Specialty specialty) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.doctorListScreen,
|
|
||||||
arguments: {
|
|
||||||
'specialty': specialty.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: specialty.color.withOpacity(0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
right: -20,
|
|
||||||
top: -20,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 40,
|
|
||||||
backgroundColor: specialty.color.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: specialty.color.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
specialty.icon,
|
|
||||||
color: specialty.color,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
specialty.name,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
specialty.description,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SliverAnimationBuilder extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const SliverAnimationBuilder({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SliverAnimatedList(
|
|
||||||
initialItemCount: 1,
|
|
||||||
itemBuilder: (context, index, animation) {
|
|
||||||
return SlideInUp(
|
|
||||||
from: 50,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/patient_dashboard/patient_home_screen.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/patient_dashboard/patient_profile_screen.dart';
|
|
||||||
|
|
||||||
class PatientDashboardScreen extends StatefulWidget {
|
|
||||||
const PatientDashboardScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientDashboardScreen> createState() => _PatientDashboardScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientDashboardScreenState extends State<PatientDashboardScreen> {
|
|
||||||
int _selectedIndex = 0;
|
|
||||||
final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
|
|
||||||
|
|
||||||
// Add your pages here
|
|
||||||
final List<Widget> _pages = [
|
|
||||||
const PatientHomeScreen(),
|
|
||||||
const Center(child: Text('Chat')), // Replace with your chat screen
|
|
||||||
const Center(child: Text('Records')), // Replace with your records screen
|
|
||||||
const PatientProfileScreen(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: PageTransitionSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
|
||||||
return FadeThroughTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _pages[_selectedIndex],
|
|
||||||
),
|
|
||||||
bottomNavigationBar: CurvedNavigationBar(
|
|
||||||
key: _bottomNavigationKey,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
color: Colors.blue,
|
|
||||||
buttonBackgroundColor: Colors.blue,
|
|
||||||
height: 60,
|
|
||||||
index: _selectedIndex,
|
|
||||||
items: const [
|
|
||||||
Icon(Icons.home, size: 30, color: Colors.white),
|
|
||||||
Icon(Icons.chat_bubble, size: 30, color: Colors.white),
|
|
||||||
Icon(Icons.assignment, size: 30, color: Colors.white),
|
|
||||||
Icon(Icons.person, size: 30, color: Colors.white),
|
|
||||||
],
|
|
||||||
onTap: (index) {
|
|
||||||
setState(() {
|
|
||||||
_selectedIndex = index;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,695 +0,0 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:medora/data/models/consultation_booking.dart';
|
|
||||||
import 'package:medora/data/services/consultation_booking_service.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart';
|
|
||||||
|
|
||||||
class PatientHomeScreen extends StatefulWidget {
|
|
||||||
const PatientHomeScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientHomeScreen> createState() => _PatientHomeScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientHomeScreenState extends State<PatientHomeScreen>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _animationController;
|
|
||||||
final BookingService _bookingService = BookingService();
|
|
||||||
late Stream<List<Booking>> _bookingsStream;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
_animationController.forward();
|
|
||||||
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user != null) {
|
|
||||||
final String userId = user.uid;
|
|
||||||
_bookingsStream = _bookingService.getPatientBookings(userId);
|
|
||||||
} else {
|
|
||||||
_bookingsStream = const Stream.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildSearchBar(),
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
children: [
|
|
||||||
_buildRealTimeCard(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildConsultationsSection(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildFindDoctorSection(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSearchBar() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(30.0),
|
|
||||||
bottomRight: Radius.circular(30.0),
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.withOpacity(0.3),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search Doctor/Hospital/Symptoms',
|
|
||||||
hintStyle: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.blue),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRealTimeCard() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.blue[400]!, Colors.white],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.withOpacity(0.3),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Real-time care\nat your fingertips.',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 30,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: const Color.fromARGB(221, 67, 67, 67),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const SpecialtyScreen()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.blue[700],
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
|
||||||
),
|
|
||||||
elevation: 5,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Start Consultation',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConsultationsSection() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Upcoming Consultations',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey[800],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
SizedBox(
|
|
||||||
height: 201,
|
|
||||||
child: StreamBuilder<List<Booking>>(
|
|
||||||
stream: _bookingsStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'Loading consultations...',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline,
|
|
||||||
color: Colors.red[400], size: 48),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'Error loading consultations',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.red[400],
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Implement refresh logic
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Try Again',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final bookings = snapshot.data ?? [];
|
|
||||||
|
|
||||||
if (bookings.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.calendar_today,
|
|
||||||
color: Colors.grey[400], size: 48),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'No upcoming consultations',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Navigate to book consultation
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Book a Consultation',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue[700],
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: bookings.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final booking = bookings[index];
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 16),
|
|
||||||
child: Hero(
|
|
||||||
tag: 'consultation_${booking.id}',
|
|
||||||
child: Material(
|
|
||||||
child: _consultationCard(
|
|
||||||
booking.profileImageUrl,
|
|
||||||
booking.doctorName,
|
|
||||||
'${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}',
|
|
||||||
booking.specialization,
|
|
||||||
booking.paymentStatus,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _consultationCard(
|
|
||||||
String? profileImageUrl,
|
|
||||||
String name,
|
|
||||||
String schedule,
|
|
||||||
String speciality,
|
|
||||||
PaymentStatus paymentStatus,
|
|
||||||
) {
|
|
||||||
return Container(
|
|
||||||
width: 300,
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.white, Colors.grey[50]!],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (profileImageUrl != null)
|
|
||||||
Container(
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
image: DecorationImage(
|
|
||||||
image: NetworkImage(profileImageUrl),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue[300]!.withOpacity(0.3),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Container(
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.blue[400]!, Colors.blue[600]!],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue[300]!.withOpacity(0.3),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 36,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
name,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey[800],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
speciality,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getStatusColor(paymentStatus).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
_getStatusColor(paymentStatus).withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
_getStatusIcon(paymentStatus),
|
|
||||||
size: 14,
|
|
||||||
color: _getStatusColor(paymentStatus),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
_getStatusText(paymentStatus),
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: _getStatusColor(paymentStatus),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue[50],
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.blue[100]!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.calendar_today,
|
|
||||||
size: 18,
|
|
||||||
color: Colors.blue[700],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
schedule,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.blue[700],
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData _getStatusIcon(PaymentStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case PaymentStatus.completed:
|
|
||||||
return Icons.check_circle;
|
|
||||||
case PaymentStatus.pending:
|
|
||||||
return Icons.access_time;
|
|
||||||
case PaymentStatus.failed:
|
|
||||||
return Icons.error;
|
|
||||||
default:
|
|
||||||
return Icons.info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getStatusColor(PaymentStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case PaymentStatus.pending:
|
|
||||||
return Colors.orange;
|
|
||||||
case PaymentStatus.completed:
|
|
||||||
return Colors.green;
|
|
||||||
case PaymentStatus.failed:
|
|
||||||
return Colors.red;
|
|
||||||
default:
|
|
||||||
return Colors.grey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getStatusText(PaymentStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case PaymentStatus.pending:
|
|
||||||
return 'Payment Pending';
|
|
||||||
case PaymentStatus.completed:
|
|
||||||
return 'Confirmed';
|
|
||||||
case PaymentStatus.failed:
|
|
||||||
return 'Payment Failed';
|
|
||||||
default:
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFindDoctorSection() {
|
|
||||||
final specialistData = [
|
|
||||||
{
|
|
||||||
'icon': Icons.local_hospital,
|
|
||||||
'label': 'General',
|
|
||||||
'color': Colors.blue,
|
|
||||||
'description': 'Primary Healthcare'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.remove_red_eye,
|
|
||||||
'label': 'Eye',
|
|
||||||
'color': Colors.indigo,
|
|
||||||
'description': 'Vision Care'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.medical_services,
|
|
||||||
'label': 'Dental',
|
|
||||||
'color': Colors.amber,
|
|
||||||
'description': 'Oral Health'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.favorite,
|
|
||||||
'label': 'Cardio',
|
|
||||||
'color': Colors.red,
|
|
||||||
'description': 'Heart Specialist'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.psychology,
|
|
||||||
'label': 'Mental',
|
|
||||||
'color': Colors.green,
|
|
||||||
'description': 'Mental Health'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.child_care,
|
|
||||||
'label': 'Pediatric',
|
|
||||||
'color': Colors.purple,
|
|
||||||
'description': 'Child Care'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.elderly,
|
|
||||||
'label': 'Geriatric',
|
|
||||||
'color': Colors.teal,
|
|
||||||
'description': 'Senior Care'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.fitness_center,
|
|
||||||
'label': 'Physio',
|
|
||||||
'color': Colors.orange,
|
|
||||||
'description': 'Physical Therapy'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: Text(
|
|
||||||
'Find Specialists',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
for (final data in specialistData)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: _specialistCard(
|
|
||||||
icon: data['icon'] as IconData,
|
|
||||||
label: data['label'] as String,
|
|
||||||
color: data['color'] as Color,
|
|
||||||
description: data['description'] as String,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_forward, color: Colors.blue),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const SpecialtyScreen()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _specialistCard({
|
|
||||||
required IconData icon,
|
|
||||||
required String label,
|
|
||||||
required Color color,
|
|
||||||
required String description,
|
|
||||||
}) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.doctorListScreen,
|
|
||||||
arguments: {
|
|
||||||
'specialty': label,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
width: 140,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: color.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(icon, color: color, size: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
description,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,197 +0,0 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:medora/data/services/patient_registration_service.dart';
|
|
||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
|
|
||||||
class PatientProfileScreen extends StatefulWidget {
|
|
||||||
const PatientProfileScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientProfileScreen> createState() => _PatientProfileScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientProfileScreenState extends State<PatientProfileScreen> {
|
|
||||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
||||||
PatientModel? _patientProfile;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_fetchPatientProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPatientProfile() async {
|
|
||||||
final patientProfile = await PatientProfileService.getPatientProfile();
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_patientProfile = patientProfile;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildProfileHeader(),
|
|
||||||
_buildProfileOptions(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildProfileHeader() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(0xFF00BCD4),
|
|
||||||
Color(0xFF2196F3),
|
|
||||||
],
|
|
||||||
begin: Alignment.centerLeft,
|
|
||||||
end: Alignment.centerRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(20),
|
|
||||||
bottomRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
image: _patientProfile?.profileImageUrl != null
|
|
||||||
? DecorationImage(
|
|
||||||
image: NetworkImage(_patientProfile!.profileImageUrl!),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: _patientProfile?.profileImageUrl == null
|
|
||||||
? Center(
|
|
||||||
child: Text(
|
|
||||||
_patientProfile != null && _patientProfile!.name != null
|
|
||||||
? _patientProfile!.name![0].toUpperCase()
|
|
||||||
: '',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 30,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_patientProfile != null && _patientProfile!.name != null
|
|
||||||
? _patientProfile!.name!
|
|
||||||
: 'Create your profile',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.chevron_right,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 30,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildProfileOptions() {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildOptionTile(
|
|
||||||
'Medical Profile',
|
|
||||||
Icons.medical_information_outlined,
|
|
||||||
onTap: () {
|
|
||||||
// Add navigation or action
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
_buildOptionTile(
|
|
||||||
'Sign Out',
|
|
||||||
Icons.logout,
|
|
||||||
onTap: () {
|
|
||||||
_signOut();
|
|
||||||
},
|
|
||||||
iconColor: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOptionTile(String title, IconData icon,
|
|
||||||
{required VoidCallback onTap, Color? iconColor}) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
icon,
|
|
||||||
color: iconColor ?? Colors.grey,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: const Icon(
|
|
||||||
Icons.chevron_right,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _signOut() async {
|
|
||||||
try {
|
|
||||||
await _auth.signOut();
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).pushReplacementNamed(RouteNames.launch);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print("Error signing out: $e");
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Failed to log out. Please try again.')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class PatientLandingScreen extends StatelessWidget {
|
|
||||||
const PatientLandingScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Colors.teal.shade100, Colors.white],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Card(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pushNamed(
|
|
||||||
RouteNames.patientDashboardScreen);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Skip',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.teal.shade300,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Image.asset(
|
|
||||||
'images/patient-avathar.png',
|
|
||||||
height: 200,
|
|
||||||
width: 200,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
const Text(
|
|
||||||
'Set your medical profile',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 30,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pushNamed(
|
|
||||||
RouteNames.patientRegistrationScreen);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'Continue',
|
|
||||||
style:
|
|
||||||
TextStyle(fontSize: 18, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,285 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:medora/controllers/patient_controller.dart';
|
|
||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
|
|
||||||
class FamilyMembersEditScreen extends StatefulWidget {
|
|
||||||
final FamilyMember? familyMember;
|
|
||||||
final PatientController controller;
|
|
||||||
|
|
||||||
const FamilyMembersEditScreen(
|
|
||||||
{super.key, this.familyMember, required this.controller});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FamilyMembersEditScreen> createState() =>
|
|
||||||
_FamilyMembersEditScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FamilyMembersEditScreenState extends State<FamilyMembersEditScreen> {
|
|
||||||
late TextEditingController nameController;
|
|
||||||
late TextEditingController relationController;
|
|
||||||
late TextEditingController genderController;
|
|
||||||
late TextEditingController dobController;
|
|
||||||
Map<String, String> errors = {};
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
nameController =
|
|
||||||
TextEditingController(text: widget.familyMember?.name ?? '');
|
|
||||||
relationController =
|
|
||||||
TextEditingController(text: widget.familyMember?.relation ?? '');
|
|
||||||
genderController =
|
|
||||||
TextEditingController(text: widget.familyMember?.gender ?? '');
|
|
||||||
dobController = TextEditingController(
|
|
||||||
text: widget.familyMember?.dateOfBirth?.toString().split(' ')[0] ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Edit Family Member'),
|
|
||||||
actions: _buildAppBarActions(),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildTextField(nameController, 'Name', Icons.person, 'name'),
|
|
||||||
_buildDropdownField(
|
|
||||||
'Relation',
|
|
||||||
relationController.text,
|
|
||||||
(String? newValue) {
|
|
||||||
setState(() {
|
|
||||||
relationController.text = newValue ?? '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Icons.family_restroom,
|
|
||||||
),
|
|
||||||
_buildDropdownField(
|
|
||||||
'Gender',
|
|
||||||
genderController.text,
|
|
||||||
(String? newValue) {
|
|
||||||
setState(() {
|
|
||||||
genderController.text = newValue ?? '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Icons.transgender,
|
|
||||||
),
|
|
||||||
_buildDateField(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDropdownField(
|
|
||||||
String label, String value, Function(String?) onChanged, IconData icon) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
prefixIcon: Icon(icon, color: Colors.blue),
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
value: value.isEmpty ? null : value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
items: label == 'Relation'
|
|
||||||
? <String>['Father', 'Mother', 'Son', 'Daughter', 'Other']
|
|
||||||
.map<DropdownMenuItem<String>>((String value) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: value,
|
|
||||||
child: Text(value),
|
|
||||||
);
|
|
||||||
}).toList()
|
|
||||||
: <String>['Male', 'Female', 'Other']
|
|
||||||
.map<DropdownMenuItem<String>>((String value) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: value,
|
|
||||||
child: Text(value),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDateField(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: TextField(
|
|
||||||
controller: dobController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Date of Birth',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
prefixIcon: Icon(Icons.calendar_today, color: Colors.blue),
|
|
||||||
),
|
|
||||||
readOnly: true,
|
|
||||||
onTap: () async {
|
|
||||||
DateTime? pickedDate = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: DateTime.now().subtract(const Duration(days: 365)),
|
|
||||||
firstDate: DateTime(1900),
|
|
||||||
lastDate: DateTime.now().subtract(const Duration(days: 365)),
|
|
||||||
);
|
|
||||||
if (pickedDate != null) {
|
|
||||||
setState(() {
|
|
||||||
dobController.text = pickedDate.toString().split(' ')[0];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _validateFields() {
|
|
||||||
errors.clear();
|
|
||||||
|
|
||||||
if (nameController.text.trim().isEmpty) {
|
|
||||||
errors['name'] = 'Name is required';
|
|
||||||
} else if (nameController.text.trim().length < 2) {
|
|
||||||
errors['name'] = 'Name must be at least 2 characters';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relationController.text.isEmpty) {
|
|
||||||
errors['relation'] = 'Please select a relation';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (genderController.text.isEmpty) {
|
|
||||||
errors['gender'] = 'Please select a gender';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dobController.text.isEmpty) {
|
|
||||||
errors['dob'] = 'Date of Birth is required';
|
|
||||||
} else {
|
|
||||||
final dob = DateTime.tryParse(dobController.text);
|
|
||||||
if (dob == null) {
|
|
||||||
errors['dob'] = 'Invalid date format';
|
|
||||||
} else if (dob.isAfter(DateTime.now())) {
|
|
||||||
errors['dob'] = 'Date of Birth cannot be in the future';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
return errors.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showValidationErrors() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Validation Errors'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: errors.entries
|
|
||||||
.map((error) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Text(
|
|
||||||
'• ${error.value}',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTextField(
|
|
||||||
TextEditingController controller,
|
|
||||||
String label,
|
|
||||||
IconData icon,
|
|
||||||
String errorKey,
|
|
||||||
) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
prefixIcon: Icon(
|
|
||||||
icon,
|
|
||||||
color: errors.containsKey(errorKey) ? Colors.red : Colors.blue,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color:
|
|
||||||
errors.containsKey(errorKey) ? Colors.red : Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color:
|
|
||||||
errors.containsKey(errorKey) ? Colors.red : Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color:
|
|
||||||
errors.containsKey(errorKey) ? Colors.red : Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (errors.containsKey(errorKey))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4, left: 12),
|
|
||||||
child: Text(
|
|
||||||
errors[errorKey]!,
|
|
||||||
style: const TextStyle(color: Colors.red, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildAppBarActions() {
|
|
||||||
return [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_validateFields()) {
|
|
||||||
FamilyMember newMember = FamilyMember(
|
|
||||||
name: nameController.text,
|
|
||||||
relation: relationController.text,
|
|
||||||
gender: genderController.text,
|
|
||||||
dateOfBirth: DateTime.tryParse(dobController.text),
|
|
||||||
);
|
|
||||||
Navigator.pop(context, newMember);
|
|
||||||
} else {
|
|
||||||
_showValidationErrors();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('Done', style: TextStyle(color: Colors.blue)),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
nameController.dispose();
|
|
||||||
relationController.dispose();
|
|
||||||
genderController.dispose();
|
|
||||||
dobController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,381 +0,0 @@
|
|||||||
import 'package:medora/controllers/patient_controller.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:country_state_city_picker/country_state_city_picker.dart';
|
|
||||||
|
|
||||||
class PatientAddressScreen extends StatefulWidget {
|
|
||||||
final PatientController? controller;
|
|
||||||
|
|
||||||
const PatientAddressScreen({super.key, required this.controller});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientAddressScreen> createState() => _PatientAddressScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientAddressScreenState extends State<PatientAddressScreen> {
|
|
||||||
late PatientController _controller;
|
|
||||||
late TextEditingController _houseNoController;
|
|
||||||
late TextEditingController _lineController;
|
|
||||||
late TextEditingController _townController;
|
|
||||||
late TextEditingController _pincodeController;
|
|
||||||
late TextEditingController _otherLabelController;
|
|
||||||
final String country = 'India';
|
|
||||||
String? state;
|
|
||||||
String? city;
|
|
||||||
String? addressType;
|
|
||||||
final Map<String, String> _errors = {};
|
|
||||||
bool _hasErrors = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = widget.controller ?? PatientController();
|
|
||||||
_loadSavedData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadSavedData() {
|
|
||||||
final address = _controller.model.address;
|
|
||||||
_houseNoController = TextEditingController(text: address.houseNo ?? '');
|
|
||||||
_lineController = TextEditingController(text: address.line ?? '');
|
|
||||||
_townController = TextEditingController(text: address.town ?? '');
|
|
||||||
_pincodeController = TextEditingController(text: address.pincode ?? '');
|
|
||||||
_otherLabelController =
|
|
||||||
TextEditingController(text: address.otherLabel ?? '');
|
|
||||||
state = address.state;
|
|
||||||
city = address.city;
|
|
||||||
addressType = address.addressType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Address'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: _saveAndExit,
|
|
||||||
child: const Text('Done', style: TextStyle(color: Colors.blue)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildSectionContainer(
|
|
||||||
'Address Information',
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
_buildTextField(
|
|
||||||
'House No.',
|
|
||||||
_houseNoController,
|
|
||||||
(value) => widget.controller!.updateHouseNo(value),
|
|
||||||
icon: Icons.home_outlined,
|
|
||||||
errorKey: 'houseNo',
|
|
||||||
),
|
|
||||||
_buildTextField(
|
|
||||||
'Address Line',
|
|
||||||
_lineController,
|
|
||||||
(value) => widget.controller!.updateLine(value),
|
|
||||||
icon: Icons.location_on_outlined,
|
|
||||||
errorKey: 'line',
|
|
||||||
),
|
|
||||||
_buildTextField(
|
|
||||||
'Town (Optional)',
|
|
||||||
_townController,
|
|
||||||
(value) => widget.controller!.updateTown(value),
|
|
||||||
icon: Icons.location_city_outlined,
|
|
||||||
),
|
|
||||||
_buildTextField(
|
|
||||||
'Pincode',
|
|
||||||
_pincodeController,
|
|
||||||
(value) => widget.controller!.updatePincode(value),
|
|
||||||
icon: Icons.pin_drop_outlined,
|
|
||||||
errorKey: 'pincode',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildSectionContainer(
|
|
||||||
'Location',
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
_buildCountrySelection(),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
SelectState(
|
|
||||||
onCountryChanged: (value) {
|
|
||||||
setState(() {});
|
|
||||||
widget.controller!.updateCountry('India');
|
|
||||||
},
|
|
||||||
onStateChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
state = value;
|
|
||||||
});
|
|
||||||
widget.controller!.updateState(value);
|
|
||||||
},
|
|
||||||
onCityChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
city = value;
|
|
||||||
});
|
|
||||||
widget.controller!.updateCity(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
if (state != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
|
||||||
child: Text('State: $state',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14, color: Colors.black87)),
|
|
||||||
),
|
|
||||||
if (city != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
|
||||||
child: Text('City: $city',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14, color: Colors.black87)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildSectionContainer(
|
|
||||||
'Address Type',
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
_buildAddressTypeChips(),
|
|
||||||
if (addressType == 'Other')
|
|
||||||
_buildTextField(
|
|
||||||
'Other Label',
|
|
||||||
_otherLabelController,
|
|
||||||
(value) => widget.controller!.updateOtherLabel(value),
|
|
||||||
icon: Icons.label_outline,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _validateFields() {
|
|
||||||
setState(() {
|
|
||||||
_errors.clear();
|
|
||||||
_hasErrors = false;
|
|
||||||
|
|
||||||
if (_houseNoController.text.trim().isEmpty) {
|
|
||||||
_errors['houseNo'] = 'House No. is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_lineController.text.trim().isEmpty) {
|
|
||||||
_errors['line'] = 'Address Line is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pincode = _pincodeController.text.trim();
|
|
||||||
if (pincode.isEmpty) {
|
|
||||||
_errors['pincode'] = 'Pincode is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
} else if (!RegExp(r'^\d{6}$').hasMatch(pincode)) {
|
|
||||||
_errors['pincode'] = 'Enter a valid 6-digit pincode';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == null || state!.isEmpty) {
|
|
||||||
_errors['state'] = 'State is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (city == null || city!.isEmpty) {
|
|
||||||
_errors['city'] = 'City is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressType == null || addressType!.isEmpty) {
|
|
||||||
_errors['addressType'] = 'Please select an address type';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressType == 'Other' && _otherLabelController.text.trim().isEmpty) {
|
|
||||||
_errors['otherLabel'] = 'Please specify other label';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return !_hasErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionContainer(String title, Widget content) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueGrey.withOpacity(0.5),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
content,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTextField(
|
|
||||||
String label,
|
|
||||||
TextEditingController controller,
|
|
||||||
Function(String) onChanged, {
|
|
||||||
required IconData icon,
|
|
||||||
String? errorKey,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
prefixIcon: Icon(icon,
|
|
||||||
color: _errors.containsKey(errorKey)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blueAccent),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: _errors.containsKey(errorKey)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
errorText: _errors[errorKey],
|
|
||||||
),
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCountrySelection() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: const Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Country:',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('India', style: TextStyle(fontSize: 16)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAddressTypeChips() {
|
|
||||||
return Wrap(
|
|
||||||
spacing: 8.0,
|
|
||||||
children: ['Home', 'Office', 'Other'].map((String type) {
|
|
||||||
return ChoiceChip(
|
|
||||||
label: Text(type),
|
|
||||||
selected: addressType == type,
|
|
||||||
onSelected: (bool selected) {
|
|
||||||
setState(() {
|
|
||||||
addressType = selected ? type : addressType;
|
|
||||||
});
|
|
||||||
widget.controller!.updateAddressType(addressType!);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveAndExit() {
|
|
||||||
if (_validateFields()) {
|
|
||||||
widget.controller!.updateHouseNo(_houseNoController.text);
|
|
||||||
widget.controller!.updateLine(_lineController.text);
|
|
||||||
widget.controller!.updateTown(_townController.text);
|
|
||||||
widget.controller!.updatePincode(_pincodeController.text);
|
|
||||||
widget.controller!.updateCountry(country);
|
|
||||||
widget.controller!.updateState(state ?? '');
|
|
||||||
widget.controller!.updateCity(city ?? '');
|
|
||||||
widget.controller!.updateAddressType(addressType ?? '');
|
|
||||||
widget.controller!.updateOtherLabel(_otherLabelController.text);
|
|
||||||
widget.controller!.updatePatientData();
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
} else {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Validation Errors'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: _errors.entries
|
|
||||||
.map((error) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Text(
|
|
||||||
'• ${error.value}',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_houseNoController.dispose();
|
|
||||||
_lineController.dispose();
|
|
||||||
_townController.dispose();
|
|
||||||
_pincodeController.dispose();
|
|
||||||
_otherLabelController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,308 +0,0 @@
|
|||||||
import 'package:medora/data/models/patient.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:medora/screens/patient_screens/registration_screens/family_members_edit_screen.dart';
|
|
||||||
import '../../../controllers/patient_controller.dart';
|
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
|
||||||
|
|
||||||
class PatientFamilyMembersScreen extends StatefulWidget {
|
|
||||||
final PatientController controller;
|
|
||||||
const PatientFamilyMembersScreen({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientFamilyMembersScreen> createState() =>
|
|
||||||
_PatientFamilyMembersScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientFamilyMembersScreenState
|
|
||||||
extends State<PatientFamilyMembersScreen> {
|
|
||||||
bool isLoading = false;
|
|
||||||
final int maxFamilyMembers = 5;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text(
|
|
||||||
'Family Members',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
|
||||||
onPressed: () {
|
|
||||||
if (_validateFamilyMembers()) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
actions: _buildAppBarActions(),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: widget.controller.model.familyMembers.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return FamilyMemberCard(
|
|
||||||
familyMember: widget.controller.model.familyMembers[index],
|
|
||||||
onEdit: () => _editFamilyMember(index),
|
|
||||||
onDelete: () => _deleteFamilyMember(index),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _addFamilyMember,
|
|
||||||
backgroundColor: Colors.blue,
|
|
||||||
child: const Icon(Icons.add, color: Colors.white),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildAppBarActions() {
|
|
||||||
return [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_validateFamilyMembers()) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Done',
|
|
||||||
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _validateFamilyMembers() {
|
|
||||||
if (widget.controller.model.familyMembers.isEmpty) {
|
|
||||||
_showValidationError('Please add at least one family member');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final relations = widget.controller.model.familyMembers
|
|
||||||
.map((member) => member.relation?.toLowerCase())
|
|
||||||
.toList();
|
|
||||||
if (relations.toSet().length != relations.length) {
|
|
||||||
_showValidationError('Duplicate relations are not allowed');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showValidationError(String message) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Validation Error'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Text(message),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addFamilyMember() {
|
|
||||||
if (widget.controller.model.familyMembers.length >= maxFamilyMembers) {
|
|
||||||
_showValidationError('Maximum $maxFamilyMembers family members allowed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => FamilyMembersEditScreen(
|
|
||||||
controller: widget.controller,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((newMember) {
|
|
||||||
if (newMember != null) {
|
|
||||||
setState(() {
|
|
||||||
widget.controller.addFamilyMember(newMember);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editFamilyMember(int index) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => FamilyMembersEditScreen(
|
|
||||||
controller: widget.controller,
|
|
||||||
familyMember: widget.controller.model.familyMembers[index],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).then((editedMember) {
|
|
||||||
if (editedMember != null) {
|
|
||||||
setState(() {
|
|
||||||
widget.controller.updateFamilyMember(index, editedMember);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteFamilyMember(int index) {
|
|
||||||
if (widget.controller.model.familyMembers.length <= 1) {
|
|
||||||
_showValidationError('At least one family member is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Delete Family Member'),
|
|
||||||
content:
|
|
||||||
const Text('Are you sure you want to delete this family member?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
widget.controller.deleteFamilyMember(index);
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FamilyMemberCard extends StatelessWidget {
|
|
||||||
final FamilyMember familyMember;
|
|
||||||
final VoidCallback onEdit;
|
|
||||||
final VoidCallback onDelete;
|
|
||||||
|
|
||||||
const FamilyMemberCard({
|
|
||||||
super.key,
|
|
||||||
required this.familyMember,
|
|
||||||
required this.onEdit,
|
|
||||||
required this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget _buildInfoRow(IconData icon, String label, String? value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 20, color: Colors.blueGrey),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.blueGrey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value ?? 'Not provided',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 15,
|
|
||||||
color: value == null || value.isEmpty
|
|
||||||
? Colors.redAccent
|
|
||||||
: Colors.black87,
|
|
||||||
fontStyle: value == null || value.isEmpty
|
|
||||||
? FontStyle.italic
|
|
||||||
: FontStyle.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Slidable(
|
|
||||||
key: ValueKey(familyMember),
|
|
||||||
endActionPane: ActionPane(
|
|
||||||
motion: const ScrollMotion(),
|
|
||||||
extentRatio: 0.3,
|
|
||||||
children: [
|
|
||||||
SlidableAction(
|
|
||||||
onPressed: (context) => onEdit(),
|
|
||||||
foregroundColor: Colors.blue,
|
|
||||||
icon: Icons.edit,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
spacing: 0,
|
|
||||||
),
|
|
||||||
SlidableAction(
|
|
||||||
onPressed: (context) => onDelete(),
|
|
||||||
foregroundColor: Colors.red,
|
|
||||||
icon: Icons.delete,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
spacing: 0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Card(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: Colors.blueGrey[50],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
_buildInfoRow(Icons.person, 'Name:', familyMember.name),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoRow(
|
|
||||||
Icons.transgender, 'Gender:', familyMember.gender),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoRow(
|
|
||||||
Icons.cake,
|
|
||||||
'Date of Birth:',
|
|
||||||
familyMember.dateOfBirth?.toString().split(' ')[0] ??
|
|
||||||
'Not provided',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoRow(
|
|
||||||
Icons.family_restroom, 'Relation:', familyMember.relation),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,680 +0,0 @@
|
|||||||
import 'package:medora/route/route_names.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import '../../../controllers/patient_controller.dart';
|
|
||||||
import '../../../widgets/alert_screen.dart';
|
|
||||||
|
|
||||||
class PatientRegistrationScreen extends StatefulWidget {
|
|
||||||
const PatientRegistrationScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PatientRegistrationScreen> createState() =>
|
|
||||||
_PatientRegistrationScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PatientRegistrationScreenState extends State<PatientRegistrationScreen> {
|
|
||||||
final PatientController _controller = PatientController();
|
|
||||||
final TextEditingController _nameController = TextEditingController();
|
|
||||||
final TextEditingController _phoneController = TextEditingController();
|
|
||||||
bool _hasErrors = false;
|
|
||||||
final Map<String, String> _errors = {};
|
|
||||||
|
|
||||||
String? _gender;
|
|
||||||
DateTime? _dateOfBirth;
|
|
||||||
File? _image;
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
|
||||||
String _selectedCountryCode = '+1';
|
|
||||||
|
|
||||||
final List<String> _countryCodes = ['+1', '+91', '+44', '+61', '+81'];
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_nameController.text = _controller.model.name ?? '';
|
|
||||||
if (_controller.model.phoneNumber != null) {
|
|
||||||
String phoneNumber = _controller.model.phoneNumber!;
|
|
||||||
if (phoneNumber.startsWith('+')) {
|
|
||||||
for (String code in _countryCodes) {
|
|
||||||
if (phoneNumber.startsWith(code)) {
|
|
||||||
_selectedCountryCode = code;
|
|
||||||
_phoneController.text = phoneNumber.substring(code.length);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_phoneController.text = phoneNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_gender = _controller.model.gender;
|
|
||||||
_dateOfBirth = _controller.model.dateOfBirth;
|
|
||||||
if (_controller.model.profileImagePath != null) {
|
|
||||||
_image = File(_controller.model.profileImagePath!);
|
|
||||||
}
|
|
||||||
_updateCombinedPhoneNumber(_phoneController.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _getImage(ImageSource source) async {
|
|
||||||
final XFile? pickedFile = await _picker.pickImage(source: source);
|
|
||||||
|
|
||||||
if (pickedFile != null) {
|
|
||||||
setState(() {
|
|
||||||
_image = File(pickedFile.path);
|
|
||||||
});
|
|
||||||
_controller.updateProfileImage(pickedFile.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateCombinedPhoneNumber(String phoneNumber) {
|
|
||||||
String cleanPhoneNumber = phoneNumber.replaceAll(RegExp(r'^\+\d{1,3}'), '');
|
|
||||||
String fullPhoneNumber = '$_selectedCountryCode$cleanPhoneNumber';
|
|
||||||
_controller.updatePhoneNumber(fullPhoneNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showImageSourceActionSheet(BuildContext context) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Text(
|
|
||||||
'Select Image Source',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.photo_library, color: Colors.blue),
|
|
||||||
),
|
|
||||||
title: const Text('Choose from Gallery'),
|
|
||||||
onTap: () {
|
|
||||||
_getImage(ImageSource.gallery);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.photo_camera, color: Colors.blue),
|
|
||||||
),
|
|
||||||
title: const Text('Take a Photo'),
|
|
||||||
onTap: () {
|
|
||||||
_getImage(ImageSource.camera);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showResultDialog(bool isSuccess) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => AlertScreen(
|
|
||||||
arguments: AlertArguments(
|
|
||||||
title: isSuccess ? 'Thank You' : 'Oops!',
|
|
||||||
message: isSuccess
|
|
||||||
? 'Profile created successfully!'
|
|
||||||
: 'Failed to create profile. Please try again.',
|
|
||||||
actionTitle: isSuccess ? 'Go to Dashboard' : 'Try Again',
|
|
||||||
type: isSuccess ? AlertType.success : AlertType.error,
|
|
||||||
onActionPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
if (isSuccess) {
|
|
||||||
Navigator.pushReplacementNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.patientDashboardScreen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.grey[50],
|
|
||||||
appBar: AppBar(
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
title: const Text(
|
|
||||||
'Create Profile',
|
|
||||||
style: TextStyle(color: Colors.black),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_validateAllFields()) {
|
|
||||||
_controller.savePatientData();
|
|
||||||
_showResultDialog(true);
|
|
||||||
} else {
|
|
||||||
_showValidationErrors();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.check, color: Colors.blue, weight: 50),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Colors.white,
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _showImageSourceActionSheet(context),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueGrey.withOpacity(0.5),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.blue, width: 2),
|
|
||||||
),
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 75,
|
|
||||||
backgroundImage:
|
|
||||||
_image != null ? FileImage(_image!) : null,
|
|
||||||
child: _image == null
|
|
||||||
? const Icon(Icons.person,
|
|
||||||
size: 50, color: Colors.blue)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueGrey.withOpacity(0.5),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
color: Colors.blue,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.camera_alt,
|
|
||||||
size: 20, color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueGrey.withOpacity(0.5),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildUniformField(
|
|
||||||
label: 'Name',
|
|
||||||
icon: Icons.person_outline,
|
|
||||||
child: TextField(
|
|
||||||
controller: _nameController,
|
|
||||||
onChanged: (value) => _controller.updateName(value),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'Enter your name',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildUniformField(
|
|
||||||
label: 'Phone Number',
|
|
||||||
icon: Icons.phone_outlined,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: _selectedCountryCode,
|
|
||||||
onChanged: (String? newValue) {
|
|
||||||
if (newValue != null) {
|
|
||||||
setState(() {
|
|
||||||
_selectedCountryCode = newValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
_updateCombinedPhoneNumber(
|
|
||||||
_phoneController.text);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
items:
|
|
||||||
_countryCodes.map<DropdownMenuItem<String>>(
|
|
||||||
(String code) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: code,
|
|
||||||
child: Text(code),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _phoneController,
|
|
||||||
onChanged: (value) {
|
|
||||||
_updateCombinedPhoneNumber(value);
|
|
||||||
},
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: 'Enter your phone number',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildUniformField(
|
|
||||||
label: 'Gender',
|
|
||||||
icon: Icons.people_outline,
|
|
||||||
child: DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<String>(
|
|
||||||
value: _gender,
|
|
||||||
isExpanded: true,
|
|
||||||
hint: const Text('Select gender'),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => _gender = value);
|
|
||||||
_controller.updateGender(value!);
|
|
||||||
},
|
|
||||||
items: ['Male', 'Female', 'Other']
|
|
||||||
.map<DropdownMenuItem<String>>((String value) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: value,
|
|
||||||
child: Text(value),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildUniformField(
|
|
||||||
label: 'Date of Birth',
|
|
||||||
icon: Icons.calendar_today_outlined,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () async {
|
|
||||||
final DateTime? picked = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: _dateOfBirth ??
|
|
||||||
DateTime.now()
|
|
||||||
.subtract(const Duration(days: 365 * 18)),
|
|
||||||
firstDate: DateTime(1900),
|
|
||||||
lastDate: DateTime.now()
|
|
||||||
.subtract(const Duration(days: 365 * 18)),
|
|
||||||
builder: (context, child) {
|
|
||||||
return Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
colorScheme: const ColorScheme.light(
|
|
||||||
primary: Colors.blue),
|
|
||||||
),
|
|
||||||
child: child!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (picked != null && picked != _dateOfBirth) {
|
|
||||||
setState(() => _dateOfBirth = picked);
|
|
||||||
_controller.updateDateOfBirth(picked);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Text(
|
|
||||||
_dateOfBirth != null
|
|
||||||
? DateFormat('dd/MM/yyyy').format(_dateOfBirth!)
|
|
||||||
: 'Select date of birth',
|
|
||||||
style: TextStyle(
|
|
||||||
color: _dateOfBirth != null
|
|
||||||
? Colors.black87
|
|
||||||
: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blueGrey.withOpacity(0.5),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildNavigationField(
|
|
||||||
'Address',
|
|
||||||
Icons.location_on,
|
|
||||||
() async {
|
|
||||||
final result = await Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.patientAdressScreen,
|
|
||||||
arguments: _controller,
|
|
||||||
);
|
|
||||||
if (result == true) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
_buildNavigationField(
|
|
||||||
'Family Members',
|
|
||||||
Icons.family_restroom_outlined,
|
|
||||||
() => Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
RouteNames.patientFamilyMembersScreen,
|
|
||||||
arguments: _controller,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildUniformField({
|
|
||||||
required String label,
|
|
||||||
required IconData icon,
|
|
||||||
required Widget child,
|
|
||||||
String? errorKey,
|
|
||||||
}) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[50],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: _errors.containsKey(errorKey ?? '')
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey.shade200,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, top: 8),
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: _errors.containsKey(errorKey ?? '')
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 20,
|
|
||||||
color: _errors.containsKey(errorKey ?? '')
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blue,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: child),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_errors.containsKey(errorKey ?? ''))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
|
||||||
child: Text(
|
|
||||||
_errors[errorKey]!,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _validateAllFields() {
|
|
||||||
setState(() {
|
|
||||||
_errors.clear();
|
|
||||||
_hasErrors = false;
|
|
||||||
|
|
||||||
final name = _nameController.text.trim();
|
|
||||||
if (name.isEmpty) {
|
|
||||||
_errors['name'] = 'Name is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
} else if (name.length < 2 &&
|
|
||||||
RegExp(r'^[A-Za-z]+([.\s]?[A-Za-z]+)*$').hasMatch(name)) {
|
|
||||||
_errors['name'] = 'Name must be at least 2 characters';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final phoneNumber = _phoneController.text.trim();
|
|
||||||
if (phoneNumber.isEmpty) {
|
|
||||||
_errors['phone'] = 'Phone number is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
} else if (!RegExp(r'^\d{10}$').hasMatch(phoneNumber)) {
|
|
||||||
_errors['phone'] = 'Enter a valid 10-digit phone number';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gender == null) {
|
|
||||||
_errors['gender'] = 'Please select a gender';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dateOfBirth == null) {
|
|
||||||
_errors['dob'] = 'Date of Birth is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
} else {
|
|
||||||
final age = DateTime.now().difference(_dateOfBirth!).inDays ~/ 365;
|
|
||||||
if (age < 18) {
|
|
||||||
_errors['dob'] = 'User must be at least 18 years old';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_image == null) {
|
|
||||||
_errors['image'] = 'Profile picture is required';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final address = _controller.model.address;
|
|
||||||
if (address.houseNo?.isEmpty ?? true) {
|
|
||||||
_errors['address'] = 'Please complete all required address fields';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.addressType == 'Other' &&
|
|
||||||
(address.otherLabel?.isEmpty ?? true)) {
|
|
||||||
_errors['address'] = 'Please specify other address label';
|
|
||||||
_hasErrors = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return !_hasErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showValidationErrors() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline, color: Colors.red),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Validation Errors'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: _errors.entries
|
|
||||||
.map((error) => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Text(
|
|
||||||
'• ${error.value}',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNavigationField(
|
|
||||||
String label, IconData icon, VoidCallback onTap) {
|
|
||||||
bool isAddressField = label == 'Address';
|
|
||||||
bool hasAddressError = _errors.containsKey('address');
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: (isAddressField && hasAddressError)
|
|
||||||
? Colors.red.withOpacity(0.1)
|
|
||||||
: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Icon(icon,
|
|
||||||
color: (isAddressField && hasAddressError)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.blue,
|
|
||||||
size: 24),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: (isAddressField && hasAddressError)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: isAddressField ? _buildAddressSubtitle() : null,
|
|
||||||
trailing: Icon(
|
|
||||||
Icons.chevron_right,
|
|
||||||
color:
|
|
||||||
(isAddressField && hasAddressError) ? Colors.red : Colors.blue,
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
|
||||||
),
|
|
||||||
if (isAddressField && hasAddressError)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
|
||||||
child: Text(
|
|
||||||
_errors['address']!,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAddressSubtitle() {
|
|
||||||
final address = _controller.model.address;
|
|
||||||
if (address.houseNo == null ||
|
|
||||||
address.line == null ||
|
|
||||||
address.city == null) {
|
|
||||||
return const Text(
|
|
||||||
'No address added',
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
'${address.houseNo}, ${address.line}\n'
|
|
||||||
'${address.city}, ${address.state} ${address.pincode}\n'
|
|
||||||
'${address.addressType}${address.addressType == "Other" ? ": ${address.otherLabel}" : ""}',
|
|
||||||
style: const TextStyle(color: Colors.black87),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,9 +20,8 @@ class _TelemednetAppState extends State<TelemednetApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
initialRoute: RouteNames.splashScreen,
|
initialRoute: RouteNames.signIn,
|
||||||
routes: {
|
routes: {
|
||||||
RouteNames.splashScreen: (context) => const SplashScreen(),
|
|
||||||
...routes,
|
...routes,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
38
pubspec.lock
38
pubspec.lock
@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.0"
|
||||||
country_state_city:
|
country_state_city:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -423,10 +423,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_slidable
|
name: flutter_slidable
|
||||||
sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3"
|
sha256: ab7dbb16f783307c9d7762ede2593ce32c220ba2ba0fd540a3db8e9a3acba71a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "4.0.0"
|
||||||
flutter_staggered_animations:
|
flutter_staggered_animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -601,18 +601,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.5"
|
version: "10.0.7"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.8"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -761,7 +761,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.0"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -782,10 +782,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.12.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -798,10 +798,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -814,10 +814,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.3"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -870,10 +870,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.5"
|
version: "14.3.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -915,5 +915,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.3 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.27.0"
|
||||||
|
|||||||
@ -47,7 +47,7 @@ dependencies:
|
|||||||
country_state_city: ^0.1.6
|
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
|
intl_phone_field: ^3.2.0
|
||||||
flutter_slidable: ^3.1.1
|
flutter_slidable: ^4.0.0
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
curved_navigation_bar: ^1.0.6
|
curved_navigation_bar: ^1.0.6
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user