feature/medora-125 (#9)
fixed bugs Co-authored-by: DhanshCOSQ <dhanshas@cosq.net> Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com> Reviewed-on: cosqnet/telemednet#9 Reviewed-by: Benoy Bose <benoybose@cosq.net> Co-authored-by: Jipson George <jipsongeorge@cosq.net> Co-committed-by: Jipson George <jipsongeorge@cosq.net>
This commit is contained in:
parent
42543367a4
commit
520c9b6e44
@ -1,4 +1,4 @@
|
|||||||
import '../data/models/consultation_center.dart';
|
import 'package:medora/data/models/consultation_center.dart';
|
||||||
|
|
||||||
class ConsultationCenterController {
|
class ConsultationCenterController {
|
||||||
final ConsultationCenter model;
|
final ConsultationCenter model;
|
||||||
|
|||||||
@ -97,11 +97,11 @@ class DoctorController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void addQualification(String qualification) {
|
void addQualification(String qualification) {
|
||||||
model.qualifications!.add(qualification.trim());
|
model.qualifications!.add(qualification);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeQualification(String qualification) {
|
void removeQualification(String qualification) {
|
||||||
model.qualifications!.remove(qualification.trim());
|
model.qualifications!.remove(qualification);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateFloorBuilding(String floorBuilding) {
|
void updateFloorBuilding(String floorBuilding) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ class Doctor {
|
|||||||
List<String>? achievements;
|
List<String>? achievements;
|
||||||
String? uid;
|
String? uid;
|
||||||
String? profileImage;
|
String? profileImage;
|
||||||
|
String? profileImageUrl;
|
||||||
String? speciality;
|
String? speciality;
|
||||||
String? yearsOfExperience;
|
String? yearsOfExperience;
|
||||||
String? licenseNumber;
|
String? licenseNumber;
|
||||||
@ -23,6 +24,7 @@ class Doctor {
|
|||||||
Doctor({
|
Doctor({
|
||||||
this.addressType,
|
this.addressType,
|
||||||
this.achievements,
|
this.achievements,
|
||||||
|
this.profileImageUrl,
|
||||||
this.profileImage, // Initialize with empty list
|
this.profileImage, // Initialize with empty list
|
||||||
this.speciality,
|
this.speciality,
|
||||||
this.yearsOfExperience,
|
this.yearsOfExperience,
|
||||||
@ -44,6 +46,7 @@ class Doctor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
'profileImagePath': profileImageUrl,
|
||||||
'profileImage': profileImage,
|
'profileImage': profileImage,
|
||||||
'achievements': achievements,
|
'achievements': achievements,
|
||||||
'speciality': speciality,
|
'speciality': speciality,
|
||||||
@ -67,6 +70,7 @@ class Doctor {
|
|||||||
|
|
||||||
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
|
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
|
||||||
achievements: List<String>.from(json['achievements'] ?? []),
|
achievements: List<String>.from(json['achievements'] ?? []),
|
||||||
|
profileImageUrl: json['profileImageUrl'],
|
||||||
profileImage: json['profileImage'],
|
profileImage: json['profileImage'],
|
||||||
speciality: json['speciality'],
|
speciality: json['speciality'],
|
||||||
yearsOfExperience: json['yearsOfExperience'],
|
yearsOfExperience: json['yearsOfExperience'],
|
||||||
|
|||||||
@ -2,7 +2,7 @@ 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:medora/controllers/consultation_center_controller.dart';
|
import 'package:medora/controllers/consultation_center_controller.dart';
|
||||||
import '../models/consultation_center.dart';
|
import 'package:medora/data/models/consultation_center.dart';
|
||||||
|
|
||||||
class ConsultationCenterService {
|
class ConsultationCenterService {
|
||||||
static final String consultationCenterCollectionName =
|
static final String consultationCenterCollectionName =
|
||||||
|
|||||||
@ -1,13 +1,184 @@
|
|||||||
|
import 'dart:io';
|
||||||
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:firebase_storage/firebase_storage.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
import 'package:medora/data/models/doctor.dart';
|
import 'package:medora/data/models/doctor.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class DoctorProfileService {
|
class DoctorProfileService {
|
||||||
static final String doctorProfileCollectionName =
|
static final String doctorProfileCollectionName =
|
||||||
dotenv.env['DOCTOR_PROFILE_COLLECTION_NAME']!;
|
dotenv.env['DOCTOR_PROFILE_COLLECTION_NAME']!;
|
||||||
static final FirebaseFirestore db = FirebaseFirestore.instance;
|
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 =
|
||||||
|
'doctor_profile_${uid}_${DateTime.now().millisecondsSinceEpoch}${path.extension(imageFile.path)}';
|
||||||
|
final Reference storageRef =
|
||||||
|
storage.ref().child('doctor_profile_images/$fileName');
|
||||||
|
|
||||||
|
final UploadTask uploadTask = storageRef.putFile(
|
||||||
|
imageFile,
|
||||||
|
SettableMetadata(
|
||||||
|
contentType: 'image/${path.extension(imageFile.path).substring(1)}',
|
||||||
|
customMetadata: {
|
||||||
|
'userId': uid,
|
||||||
|
'userType': 'doctor',
|
||||||
|
'uploadedAt': DateTime.now().toIso8601String(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TaskSnapshot snapshot = await uploadTask;
|
||||||
|
final String downloadUrl = await snapshot.ref.getDownloadURL();
|
||||||
|
|
||||||
|
print('Doctor profile image uploaded successfully');
|
||||||
|
return downloadUrl;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error uploading doctor profile image: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> deleteProfileImage(String imageUrl) async {
|
||||||
|
try {
|
||||||
|
final Reference storageRef = storage.refFromURL(imageUrl);
|
||||||
|
await storageRef.delete();
|
||||||
|
print('Doctor profile image deleted successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting doctor profile image: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> saveDoctorProfile(DoctorController controller) async {
|
||||||
|
try {
|
||||||
|
final User? user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('No user logged in');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String uid = user.uid;
|
||||||
|
final Doctor doctorData = controller.model;
|
||||||
|
String? imageUrl;
|
||||||
|
|
||||||
|
if (doctorData.profileImage != null) {
|
||||||
|
final File imageFile = File(doctorData.profileImage!);
|
||||||
|
imageUrl = await uploadProfileImage(imageFile);
|
||||||
|
if (imageUrl == null) {
|
||||||
|
print('Failed to upload doctor profile image');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> doctorJson = doctorData.toJson();
|
||||||
|
doctorJson['createdAt'] = FieldValue.serverTimestamp();
|
||||||
|
doctorJson['updatedAt'] = FieldValue.serverTimestamp();
|
||||||
|
doctorJson['uid'] = uid;
|
||||||
|
doctorJson['profileImageUrl'] = imageUrl;
|
||||||
|
|
||||||
|
await db.collection(doctorProfileCollectionName).doc(uid).set(doctorJson);
|
||||||
|
|
||||||
|
print('Doctor profile saved successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saving doctor profile: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> updateDoctorProfile(Doctor doctor) 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 (doctor.profileImage != null) {
|
||||||
|
// Delete old profile image if exists
|
||||||
|
final DocumentSnapshot oldDoc =
|
||||||
|
await db.collection(doctorProfileCollectionName).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload new profile image
|
||||||
|
final File imageFile = File(doctor.profileImage!);
|
||||||
|
imageUrl = await uploadProfileImage(imageFile);
|
||||||
|
if (imageUrl == null) {
|
||||||
|
print('Failed to upload new doctor profile image');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> doctorJson = doctor.toJson();
|
||||||
|
doctorJson['updatedAt'] = FieldValue.serverTimestamp();
|
||||||
|
if (imageUrl != null) {
|
||||||
|
doctorJson['profileImageUrl'] = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.collection(doctorProfileCollectionName)
|
||||||
|
.doc(uid)
|
||||||
|
.update(doctorJson);
|
||||||
|
|
||||||
|
print('Doctor profile updated successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating doctor profile: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> deleteDoctorProfile() async {
|
||||||
|
try {
|
||||||
|
final User? user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('No user logged in');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String uid = user.uid;
|
||||||
|
// Delete profile image if exists
|
||||||
|
final DocumentSnapshot doc =
|
||||||
|
await db.collection(doctorProfileCollectionName).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(doctorProfileCollectionName).doc(uid).delete();
|
||||||
|
|
||||||
|
print('Doctor profile deleted successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting doctor profile: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Doctor?> getDoctorProfile() async {
|
static Future<Doctor?> getDoctorProfile() async {
|
||||||
try {
|
try {
|
||||||
@ -22,7 +193,7 @@ class DoctorProfileService {
|
|||||||
await db.collection(doctorProfileCollectionName).doc(uid).get();
|
await db.collection(doctorProfileCollectionName).doc(uid).get();
|
||||||
|
|
||||||
if (!doc.exists) {
|
if (!doc.exists) {
|
||||||
print('No patient profile found for this user');
|
print('No doctor profile found for this user');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,72 +205,16 @@ class DoctorProfileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future saveDoctorProfile(DoctorController controller) async {
|
static Future<bool> isLicenseNumberDuplicate(String licenseNumber) async {
|
||||||
try {
|
try {
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
final querySnapshot = await db
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
final Doctor doctorData = controller.model;
|
|
||||||
|
|
||||||
final Map<String, dynamic> doctorJson = doctorData.toJson();
|
|
||||||
doctorJson['createdAt'] = FieldValue.serverTimestamp();
|
|
||||||
doctorJson['updatedAt'] = FieldValue.serverTimestamp();
|
|
||||||
doctorJson['uid'] = uid;
|
|
||||||
|
|
||||||
await db.collection(doctorProfileCollectionName).doc(uid).set(doctorJson);
|
|
||||||
print('Doctor profile saved successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error saving doctor profile: $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future updateDoctorProfile(DoctorController controller) async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
final Doctor doctorData = controller.model;
|
|
||||||
|
|
||||||
final Map<String, dynamic> doctorJson = doctorData.toJson();
|
|
||||||
doctorJson['updatedAt'] = FieldValue.serverTimestamp();
|
|
||||||
|
|
||||||
await db
|
|
||||||
.collection(doctorProfileCollectionName)
|
.collection(doctorProfileCollectionName)
|
||||||
.doc(uid)
|
.where('licenseNumber', isEqualTo: licenseNumber)
|
||||||
.update(doctorJson);
|
.get();
|
||||||
print('Doctor profile updated successfully');
|
|
||||||
return true;
|
return querySnapshot.docs.isNotEmpty;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error updating doctor profile: $e');
|
print('Error checking license number: $e');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future deleteDoctorProfile() async {
|
|
||||||
try {
|
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
|
||||||
if (user == null) {
|
|
||||||
print('No user logged in');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uid = user.uid;
|
|
||||||
|
|
||||||
await db.collection(doctorProfileCollectionName).doc(uid).delete();
|
|
||||||
print('Doctor profile deleted successfully');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error deleting doctor profile: $e');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ 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';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_day_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_day_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_schedule.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_centers_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_dashboard/doctor_dashboard_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_dashboard/doctor_dashboard_screen.dart';
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||||
import 'package:medora/data/services/data_service.dart';
|
import 'package:medora/data/services/data_service.dart';
|
||||||
@ -24,6 +26,123 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
|
|
||||||
|
// Add focus nodes to handle keyboard navigation
|
||||||
|
final _emailFocusNode = FocusNode();
|
||||||
|
final _passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
// Email validation patterns
|
||||||
|
final RegExp _emailRegex = RegExp(
|
||||||
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Common email domain validation
|
||||||
|
final List<String> _commonEmailDomains = [
|
||||||
|
'gmail.com',
|
||||||
|
'yahoo.com',
|
||||||
|
'hotmail.com',
|
||||||
|
'outlook.com'
|
||||||
|
];
|
||||||
|
|
||||||
|
String? _validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email address is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
// Basic format validation
|
||||||
|
if (!_emailRegex.hasMatch(value)) {
|
||||||
|
return 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email length
|
||||||
|
if (value.length > 254) {
|
||||||
|
return 'Email address is too long';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split email into local and domain parts
|
||||||
|
final parts = value.split('@');
|
||||||
|
if (parts.length != 2) {
|
||||||
|
return 'Invalid email format';
|
||||||
|
}
|
||||||
|
|
||||||
|
final localPart = parts[0];
|
||||||
|
final domainPart = parts[1].toLowerCase();
|
||||||
|
|
||||||
|
// Validate local part
|
||||||
|
if (localPart.isEmpty || localPart.length > 64) {
|
||||||
|
return 'Invalid email username';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common typos in popular domains
|
||||||
|
for (final domain in _commonEmailDomains) {
|
||||||
|
if (domainPart.length > 3 &&
|
||||||
|
domainPart != domain &&
|
||||||
|
_calculateLevenshteinDistance(domainPart, domain) <= 2) {
|
||||||
|
return 'Did you mean @$domain?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional domain validation
|
||||||
|
if (!domainPart.contains('.')) {
|
||||||
|
return 'Invalid email domain';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 6) {
|
||||||
|
return 'Password must be at least 6 characters';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
return 'Password must contain at least one uppercase letter';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return 'Password must contain at least one number';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
|
||||||
|
return 'Password must contain at least one special character';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Levenshtein distance for typo detection
|
||||||
|
int _calculateLevenshteinDistance(String a, String b) {
|
||||||
|
if (a.isEmpty) return b.length;
|
||||||
|
if (b.isEmpty) return a.length;
|
||||||
|
|
||||||
|
List<int> previousRow = List<int>.generate(b.length + 1, (i) => i);
|
||||||
|
List<int> currentRow = List<int>.filled(b.length + 1, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
currentRow[0] = i + 1;
|
||||||
|
|
||||||
|
for (int j = 0; j < b.length; j++) {
|
||||||
|
int insertCost = previousRow[j + 1] + 1;
|
||||||
|
int deleteCost = currentRow[j] + 1;
|
||||||
|
int replaceCost = previousRow[j] + (a[i] != b[j] ? 1 : 0);
|
||||||
|
|
||||||
|
currentRow[j + 1] = [insertCost, deleteCost, replaceCost].reduce(min);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> temp = previousRow;
|
||||||
|
previousRow = currentRow;
|
||||||
|
currentRow = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousRow[b.length];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -51,6 +170,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
|
focusNode: _emailFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@ -65,22 +185,25 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
),
|
),
|
||||||
prefixIcon: const Icon(Icons.email_outlined,
|
prefixIcon: const Icon(Icons.email_outlined,
|
||||||
color: Colors.blue),
|
color: Colors.blue),
|
||||||
|
errorMaxLines: 2,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
validator: (value) {
|
textInputAction: TextInputAction.next,
|
||||||
if (value?.isEmpty ?? true) {
|
onFieldSubmitted: (_) {
|
||||||
return 'Please enter your email';
|
_passwordFocusNode.requestFocus();
|
||||||
|
},
|
||||||
|
validator: _validateEmail,
|
||||||
|
onChanged: (value) {
|
||||||
|
// Trigger validation on change if the field was previously invalid
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
|
||||||
.hasMatch(value!)) {
|
|
||||||
return 'Please enter a valid email';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
|
focusNode: _passwordFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@ -106,16 +229,15 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
onPressed: () => setState(
|
onPressed: () => setState(
|
||||||
() => _obscurePassword = !_obscurePassword),
|
() => _obscurePassword = !_obscurePassword),
|
||||||
),
|
),
|
||||||
|
errorMaxLines: 2,
|
||||||
),
|
),
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
validator: (value) {
|
validator: _validatePassword,
|
||||||
if (value?.isEmpty ?? true) {
|
onChanged: (value) {
|
||||||
return 'Please enter your password';
|
// Trigger validation on change if the field was previously invalid
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
if ((value?.length ?? 0) < 6) {
|
|
||||||
return 'Password must be at least 6 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -140,7 +262,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
},
|
},
|
||||||
validator: (phone) {
|
validator: (phone) {
|
||||||
if (phone?.completeNumber.isEmpty ?? true) {
|
if (phone?.completeNumber.isEmpty ?? true) {
|
||||||
return 'Please enter your phone number';
|
return 'Phone number is required';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -165,6 +287,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
|
|
||||||
Future<void> _handleSignUp() async {
|
Future<void> _handleSignUp() async {
|
||||||
if (_formKey.currentState == null || !_formKey.currentState!.validate()) {
|
if (_formKey.currentState == null || !_formKey.currentState!.validate()) {
|
||||||
|
_showErrorSnackBar('Please fix the errors in the form');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +345,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_emailFocusNode.dispose();
|
||||||
|
_passwordFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/consultation_center_controller.dart';
|
import 'package:medora/controllers/consultation_center_controller.dart';
|
||||||
|
import 'package:medora/route/route_names.dart';
|
||||||
import '../../../route/route_names.dart';
|
|
||||||
|
|
||||||
class BusinessCenterScreen extends StatefulWidget {
|
class BusinessCenterScreen extends StatefulWidget {
|
||||||
final ConsultationCenterController controller;
|
final ConsultationCenterController controller;
|
||||||
@ -59,7 +58,7 @@ class _ConsultationCenterScreenState extends State<BusinessCenterScreen> {
|
|||||||
TextEditingController(text: center.postalCode ?? '');
|
TextEditingController(text: center.postalCode ?? '');
|
||||||
_addressTypeController =
|
_addressTypeController =
|
||||||
TextEditingController(text: center.addressType ?? '');
|
TextEditingController(text: center.addressType ?? '');
|
||||||
selectedAddressType = widget.controller?.model.addressType;
|
selectedAddressType = widget.controller.model.addressType;
|
||||||
if (selectedAddressType != null &&
|
if (selectedAddressType != null &&
|
||||||
!addressTypes.contains(selectedAddressType)) {
|
!addressTypes.contains(selectedAddressType)) {
|
||||||
showCustomTypeField = true;
|
showCustomTypeField = true;
|
||||||
@ -81,32 +80,6 @@ class _ConsultationCenterScreenState extends State<BusinessCenterScreen> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _areFieldsValid() {
|
|
||||||
return _floorBuildingController.text.isNotEmpty &&
|
|
||||||
_streetController.text.isNotEmpty &&
|
|
||||||
_cityController.text.isNotEmpty &&
|
|
||||||
_stateController.text.isNotEmpty &&
|
|
||||||
_countryController.text.isNotEmpty &&
|
|
||||||
_postalCodeController.text.isNotEmpty &&
|
|
||||||
_addressTypeController.text.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleAddressTypeSelection(String type) {
|
|
||||||
setState(() {
|
|
||||||
if (type == 'Others') {
|
|
||||||
showCustomTypeField = !showCustomTypeField;
|
|
||||||
if (!showCustomTypeField) {
|
|
||||||
_addressTypeController.clear();
|
|
||||||
selectedAddressType = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showCustomTypeField = false;
|
|
||||||
selectedAddressType = type;
|
|
||||||
widget.controller?.updateAddressType(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -151,35 +124,35 @@ class _ConsultationCenterScreenState extends State<BusinessCenterScreen> {
|
|||||||
label: 'Floor, Building',
|
label: 'Floor, Building',
|
||||||
controller: _floorBuildingController,
|
controller: _floorBuildingController,
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
widget.controller!.updateFloorBuilding(value),
|
widget.controller.updateFloorBuilding(value),
|
||||||
icon: Icons.apartment,
|
icon: Icons.apartment,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Street or Road',
|
label: 'Street or Road',
|
||||||
controller: _streetController,
|
controller: _streetController,
|
||||||
onChanged: (value) => widget.controller!.updateStreet(value),
|
onChanged: (value) => widget.controller.updateStreet(value),
|
||||||
icon: Icons.streetview,
|
icon: Icons.streetview,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'City',
|
label: 'City',
|
||||||
controller: _cityController,
|
controller: _cityController,
|
||||||
onChanged: (value) => widget.controller!.updateCity(value),
|
onChanged: (value) => widget.controller.updateCity(value),
|
||||||
icon: Icons.location_city,
|
icon: Icons.location_city,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'State',
|
label: 'State',
|
||||||
controller: _stateController,
|
controller: _stateController,
|
||||||
onChanged: (value) => widget.controller!.updateState(value),
|
onChanged: (value) => widget.controller.updateState(value),
|
||||||
icon: Icons.map,
|
icon: Icons.map,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Country',
|
label: 'Country',
|
||||||
controller: _countryController,
|
controller: _countryController,
|
||||||
onChanged: (value) => widget.controller!.updateCountry(value),
|
onChanged: (value) => widget.controller.updateCountry(value),
|
||||||
icon: Icons.flag,
|
icon: Icons.flag,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
@ -187,7 +160,7 @@ class _ConsultationCenterScreenState extends State<BusinessCenterScreen> {
|
|||||||
label: 'Postal Code',
|
label: 'Postal Code',
|
||||||
controller: _postalCodeController,
|
controller: _postalCodeController,
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
widget.controller!.updatePostalCode(value),
|
widget.controller.updatePostalCode(value),
|
||||||
icon: Icons.mail,
|
icon: Icons.mail,
|
||||||
isMandatory: true,
|
isMandatory: true,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
import 'package:medora/controllers/consultation_center_controller.dart';
|
import 'package:medora/controllers/consultation_center_controller.dart';
|
||||||
import 'package:medora/data/services/consultation_center_service.dart';
|
import 'package:medora/data/services/consultation_center_service.dart';
|
||||||
|
import 'package:medora/route/route_names.dart';
|
||||||
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_day_screen.dart';
|
import 'package:medora/screens/doctor_screen/doctor_consultation_schedule/consultation_day_screen.dart';
|
||||||
import '../../../route/route_names.dart';
|
|
||||||
|
|
||||||
class CenterFeeAndDurationScreen extends StatefulWidget {
|
class CenterFeeAndDurationScreen extends StatefulWidget {
|
||||||
final ConsultationCenterController controller;
|
final ConsultationCenterController controller;
|
||||||
@ -25,7 +25,6 @@ class CenterFeeAndDurationScreenState
|
|||||||
late final ConsultationCenterController _controller;
|
late final ConsultationCenterController _controller;
|
||||||
late TextEditingController _averageDuration;
|
late TextEditingController _averageDuration;
|
||||||
late TextEditingController _consultationFee;
|
late TextEditingController _consultationFee;
|
||||||
bool _isEditing = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -46,9 +45,7 @@ class CenterFeeAndDurationScreenState
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onFieldChanged() {
|
void _onFieldChanged() {
|
||||||
setState(() {
|
setState(() {});
|
||||||
_isEditing = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
@ -85,12 +82,14 @@ class CenterFeeAndDurationScreenState
|
|||||||
await ConsultationCenterService.saveConsultationCenters(controllers);
|
await ConsultationCenterService.saveConsultationCenters(controllers);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Doctor consultation saved successfully!'),
|
content: Text('Doctor consultation saved successfully!'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushReplacementNamed(RouteNames.scheduleConsultationScreen);
|
.pushReplacementNamed(RouteNames.scheduleConsultationScreen);
|
||||||
} else {
|
} else {
|
||||||
@ -146,6 +145,7 @@ class CenterFeeAndDurationScreenState
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _consultationFee,
|
controller: _consultationFee,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
@ -191,6 +191,7 @@ class CenterFeeAndDurationScreenState
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _averageDuration,
|
controller: _averageDuration,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:medora/common/custom_style.dart';
|
||||||
import 'package:medora/data/models/consultation_center.dart';
|
import 'package:medora/data/models/consultation_center.dart';
|
||||||
import 'package:medora/data/services/consultation_center_service.dart';
|
import 'package:medora/data/services/consultation_center_service.dart';
|
||||||
|
import 'package:medora/route/route_names.dart';
|
||||||
import '../../../common/custom_style.dart';
|
|
||||||
import '../../../route/route_names.dart';
|
|
||||||
|
|
||||||
class ScheduleConsultationScreen extends StatefulWidget {
|
class ScheduleConsultationScreen extends StatefulWidget {
|
||||||
const ScheduleConsultationScreen({super.key});
|
const ScheduleConsultationScreen({super.key});
|
||||||
@ -64,7 +63,7 @@ class ScheduleConsultationScreenState
|
|||||||
context, RouteNames.doctorDashbordScreen),
|
context, RouteNames.doctorDashbordScreen),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Schedule Consultation',
|
'Consultation Centers',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@ -114,7 +113,7 @@ class ScheduleConsultationScreenState
|
|||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: Container(
|
trailing: SizedBox(
|
||||||
width: 80, // Increased width to accommodate contents
|
width: 80, // Increased width to accommodate contents
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize:
|
mainAxisSize:
|
||||||
@ -270,10 +269,17 @@ class ScheduleConsultationScreenState
|
|||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// Pass the existing center data to maintain state
|
||||||
|
// final consultationController =
|
||||||
|
// ConsultationCenterController(
|
||||||
|
// center);
|
||||||
|
|
||||||
// Navigator.pushNamed(
|
// Navigator.pushNamed(
|
||||||
// context,
|
// context,
|
||||||
// RouteNames
|
// RouteNames
|
||||||
// .ConsultationDayScreen,
|
// .consultationDayScreen,
|
||||||
|
// arguments:
|
||||||
|
// consultationController,
|
||||||
// );
|
// );
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -52,6 +52,15 @@ class _ConsultationTimeSlotScreenState
|
|||||||
'Initial schedule for ${widget.selectedDay}: ${currentSchedule.timeSlots}');
|
'Initial schedule for ${widget.selectedDay}: ${currentSchedule.timeSlots}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String formatTime(TimeOfDay time) {
|
||||||
|
final hour = time.hourOfPeriod == 0
|
||||||
|
? 12
|
||||||
|
: time.hourOfPeriod; // Convert 0 to 12 for AM
|
||||||
|
final minute = time.minute.toString().padLeft(2, '0');
|
||||||
|
final period = time.period == DayPeriod.am ? 'AM' : 'PM';
|
||||||
|
return '$hour:$minute $period';
|
||||||
|
}
|
||||||
|
|
||||||
void _addTimeSlot() async {
|
void _addTimeSlot() async {
|
||||||
TimeOfDay? startTime = await showTimePicker(
|
TimeOfDay? startTime = await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
@ -66,74 +75,57 @@ class _ConsultationTimeSlotScreenState
|
|||||||
|
|
||||||
if (endTime != null) {
|
if (endTime != null) {
|
||||||
final slot = TimeSlot(
|
final slot = TimeSlot(
|
||||||
startTime:
|
startTime: formatTime(startTime),
|
||||||
'${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')}',
|
endTime: formatTime(endTime),
|
||||||
endTime:
|
|
||||||
'${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.controller.addTimeSlot(widget.selectedDay, slot);
|
widget.controller.addTimeSlot(widget.selectedDay, slot);
|
||||||
// Update local schedule
|
|
||||||
currentSchedule =
|
currentSchedule =
|
||||||
widget.controller.model.weeklySchedule?.firstWhere(
|
widget.controller.model.weeklySchedule!.firstWhere(
|
||||||
(schedule) => schedule.day == widget.selectedDay,
|
(schedule) => schedule.day == widget.selectedDay,
|
||||||
orElse: () => AvailabilitySchedule(
|
orElse: () => AvailabilitySchedule(
|
||||||
day: widget.selectedDay,
|
day: widget.selectedDay,
|
||||||
timeSlots: [],
|
timeSlots: [],
|
||||||
),
|
),
|
||||||
) ??
|
);
|
||||||
AvailabilitySchedule(
|
|
||||||
day: widget.selectedDay,
|
|
||||||
timeSlots: [],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug print
|
|
||||||
print('Added time slot: $slot');
|
|
||||||
print('Updated schedule: ${currentSchedule.timeSlots}');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editTimeSlot(TimeSlot currentSlot) async {
|
void _editTimeSlot(TimeSlot currentSlot) async {
|
||||||
final currentStart = currentSlot.startTime!.split(':');
|
final currentStart = currentSlot.startTime!.split(' ');
|
||||||
final currentEnd = currentSlot.endTime!.split(':');
|
currentSlot.endTime!.split(' ');
|
||||||
|
|
||||||
TimeOfDay? startTime = await showTimePicker(
|
final startTime = TimeOfDay(
|
||||||
context: context,
|
hour: int.parse(currentStart[0].split(':')[0]),
|
||||||
initialTime: TimeOfDay(
|
minute: int.parse(currentStart[0].split(':')[1]),
|
||||||
hour: int.parse(currentStart[0]),
|
|
||||||
minute: int.parse(currentStart[1]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (startTime != null) {
|
TimeOfDay? newStartTime = await showTimePicker(
|
||||||
TimeOfDay? endTime = await showTimePicker(
|
context: context,
|
||||||
|
initialTime: startTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newStartTime != null) {
|
||||||
|
TimeOfDay? newEndTime = await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: TimeOfDay(
|
initialTime: newStartTime,
|
||||||
hour: int.parse(currentEnd[0]),
|
|
||||||
minute: int.parse(currentEnd[1]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (endTime != null && mounted) {
|
if (newEndTime != null && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// Remove old slot
|
|
||||||
widget.controller.removeTimeSlot(widget.selectedDay, currentSlot);
|
widget.controller.removeTimeSlot(widget.selectedDay, currentSlot);
|
||||||
|
|
||||||
// Add new slot
|
|
||||||
final newSlot = TimeSlot(
|
final newSlot = TimeSlot(
|
||||||
startTime:
|
startTime: formatTime(newStartTime),
|
||||||
'${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')}',
|
endTime: formatTime(newEndTime),
|
||||||
endTime:
|
|
||||||
'${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}',
|
|
||||||
);
|
);
|
||||||
widget.controller.addTimeSlot(widget.selectedDay, newSlot);
|
widget.controller.addTimeSlot(widget.selectedDay, newSlot);
|
||||||
|
|
||||||
// Update local schedule
|
|
||||||
currentSchedule = widget.controller.model.weeklySchedule!.firstWhere(
|
currentSchedule = widget.controller.model.weeklySchedule!.firstWhere(
|
||||||
(schedule) => schedule.day == widget.selectedDay,
|
(schedule) => schedule.day == widget.selectedDay,
|
||||||
orElse: () => AvailabilitySchedule(
|
orElse: () => AvailabilitySchedule(
|
||||||
@ -142,9 +134,6 @@ class _ConsultationTimeSlotScreenState
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug print
|
|
||||||
// print('Edited time slot from $currentSlot to $newSlot');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,7 +303,7 @@ class _ConsultationTimeSlotScreenState
|
|||||||
title: Text(
|
title: Text(
|
||||||
'${slot.startTime} - ${slot.endTime}',
|
'${slot.startTime} - ${slot.endTime}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 13.5,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
@ -325,7 +314,7 @@ class _ConsultationTimeSlotScreenState
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.edit_outlined,
|
Icons.edit_outlined,
|
||||||
size: 30,
|
size: 26,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
onPressed: () => _editTimeSlot(slot),
|
onPressed: () => _editTimeSlot(slot),
|
||||||
@ -333,7 +322,7 @@ class _ConsultationTimeSlotScreenState
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.delete_outline,
|
Icons.delete_outline,
|
||||||
size: 30,
|
size: 26,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
onPressed: () => _deleteTimeSlot(slot),
|
onPressed: () => _deleteTimeSlot(slot),
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:medora/data/models/doctor.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart';
|
import 'package:medora/data/models/consultation_booking.dart';
|
||||||
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
|
|
||||||
class DoctorDashboardHomeScreen extends StatefulWidget {
|
class DoctorDashboardHomeScreen extends StatefulWidget {
|
||||||
const DoctorDashboardHomeScreen({super.key});
|
const DoctorDashboardHomeScreen({super.key});
|
||||||
@ -14,6 +17,7 @@ class DoctorDashboardHomeScreen extends StatefulWidget {
|
|||||||
class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
|
Doctor? _doctorProfile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -23,12 +27,23 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
|||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
|
_fetchDoctorProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_fetchDoctorProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchDoctorProfile() async {
|
||||||
|
final doctorProfile = await DoctorProfileService.getDoctorProfile();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_doctorProfile = doctorProfile;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,10 +56,17 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [_buildRealTimeCard(), const SizedBox(height: 20)],
|
children: [
|
||||||
|
_buildRealTimeCard(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildUpcomingBookings(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
// _buildCompletedConsultations()
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// _buildBottomNavBar(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -112,7 +134,7 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
|||||||
|
|
||||||
Widget _buildRealTimeCard() {
|
Widget _buildRealTimeCard() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(15), // Reduced padding
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Colors.blue[400]!, Colors.white],
|
colors: [Colors.blue[400]!, Colors.white],
|
||||||
@ -132,36 +154,286 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Real-time care\nat your fingertips.',
|
'Instant patient insights\nright at your fingertips',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 30,
|
fontSize: 24, // Reduced font size
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: const Color.fromARGB(221, 67, 67, 67),
|
color: const Color.fromARGB(221, 67, 67, 67),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8), // Reduced spacing
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
Navigator.push(
|
// // Navigator.push(
|
||||||
context,
|
// // context,
|
||||||
MaterialPageRoute(
|
// // MaterialPageRoute(
|
||||||
builder: (context) => const SpecialtyScreen()),
|
// // builder: (context) => const SpecialtyScreen()),
|
||||||
);
|
// // );
|
||||||
},
|
// },
|
||||||
style: ElevatedButton.styleFrom(
|
// // style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white,
|
// // backgroundColor: Colors.white,
|
||||||
foregroundColor: Colors.blue[700],
|
// // foregroundColor: Colors.blue[700],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7),
|
// // padding: const EdgeInsets.symmetric(
|
||||||
shape: RoundedRectangleBorder(
|
// // horizontal: 12, vertical: 5), // Reduced padding
|
||||||
borderRadius: BorderRadius.circular(30),
|
// // shape: RoundedRectangleBorder(
|
||||||
|
// // borderRadius: BorderRadius.circular(30),
|
||||||
|
// // ),
|
||||||
|
// // elevation: 5,
|
||||||
|
// // ),
|
||||||
|
// // child: Text(
|
||||||
|
// // 'Start Consultation',
|
||||||
|
// // style: GoogleFonts.poppins(
|
||||||
|
// // fontWeight: FontWeight.bold,
|
||||||
|
// // fontSize: 14, // Reduced font size
|
||||||
|
// // ),
|
||||||
|
// // ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUpcomingBookings() {
|
||||||
|
return StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
|
||||||
|
stream: FirebaseFirestore.instance
|
||||||
|
.collection('bookings')
|
||||||
|
.where('doctorId', isEqualTo: _doctorProfile?.uid!)
|
||||||
|
.orderBy('appointmentDate', descending: true)
|
||||||
|
.snapshots(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const Center(child: Text('Error loading bookings'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final bookings = snapshot.data!.docs
|
||||||
|
.map((doc) => Booking.fromMap(doc.data()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'Upcoming Bookings',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
const SizedBox(height: 12),
|
||||||
'Start Consultation',
|
if (bookings.isNotEmpty)
|
||||||
style: GoogleFonts.poppins(
|
SizedBox(
|
||||||
fontWeight: FontWeight.bold,
|
height: 220, // Adjust height as needed
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: bookings.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final booking = bookings[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: _consultationCard(
|
||||||
|
booking.patientName,
|
||||||
|
'${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}',
|
||||||
|
booking.paymentStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (bookings.isEmpty)
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'No upcoming bookings',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _consultationCard(
|
||||||
|
String name,
|
||||||
|
String schedule,
|
||||||
|
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: [
|
||||||
|
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],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:medora/data/models/doctor.dart';
|
||||||
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class DoctorPersonalProfileScreen extends StatefulWidget {
|
class DoctorPersonalProfileScreen extends StatefulWidget {
|
||||||
@ -13,6 +15,23 @@ class DoctorPersonalProfileScreen extends StatefulWidget {
|
|||||||
class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
||||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
// final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
|
// final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
|
||||||
|
Doctor? _doctorProfile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchDoctorProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchDoctorProfile() async {
|
||||||
|
final doctorProfile = await DoctorProfileService.getDoctorProfile();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_doctorProfile = doctorProfile;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -51,35 +70,45 @@ class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
|||||||
Container(
|
Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
|
image: _doctorProfile?.profileImageUrl != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: NetworkImage(_doctorProfile!.profileImageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
// child: Text(
|
||||||
'D',
|
// _doctorProfile != null && _doctorProfile!.firstName != null
|
||||||
style: TextStyle(
|
// ? _doctorProfile!.firstName![0].toUpperCase()
|
||||||
fontSize: 30,
|
// : '',
|
||||||
fontWeight: FontWeight.bold,
|
// style: const TextStyle(
|
||||||
color: Colors.blue,
|
// fontSize: 30,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// color: Colors.blue,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Doctor',
|
_doctorProfile != null && _doctorProfile!.firstName != null
|
||||||
style: TextStyle(
|
? _doctorProfile!.firstName!
|
||||||
|
: 'Welcome',
|
||||||
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const Text(
|
||||||
'Personal Profile',
|
'Personal Profile',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:medora/data/models/doctor.dart';
|
||||||
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class DoctorServicesMenuScreen extends StatefulWidget {
|
class DoctorServicesMenuScreen extends StatefulWidget {
|
||||||
@ -10,8 +11,23 @@ class DoctorServicesMenuScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
||||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
Doctor? _doctorProfile;
|
||||||
// final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
|
// final GlobalKey<CurvedNavigationBarState> _bottomNavigationKey = GlobalKey();
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchDoctorProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchDoctorProfile() async {
|
||||||
|
final doctorProfile = await DoctorProfileService.getDoctorProfile();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_doctorProfile = doctorProfile;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -50,35 +66,48 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
|||||||
Container(
|
Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
|
image: _doctorProfile?.profileImageUrl != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: NetworkImage(_doctorProfile!.profileImageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: _doctorProfile?.profileImageUrl == null
|
||||||
child: Text(
|
? Center(
|
||||||
'D',
|
child: Text(
|
||||||
style: TextStyle(
|
_doctorProfile != null &&
|
||||||
fontSize: 30,
|
_doctorProfile!.firstName != null
|
||||||
fontWeight: FontWeight.bold,
|
? _doctorProfile!.firstName![0].toUpperCase()
|
||||||
color: Colors.blue,
|
: '',
|
||||||
),
|
style: const TextStyle(
|
||||||
),
|
fontSize: 30,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Doctor',
|
_doctorProfile != null && _doctorProfile!.firstName != null
|
||||||
style: TextStyle(
|
? _doctorProfile!.firstName!
|
||||||
|
: 'Welcome',
|
||||||
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const Text(
|
||||||
'See our services below',
|
'See our services below',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@ -88,11 +117,6 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
|
||||||
Icons.chevron_right,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 30,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -115,7 +139,7 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildOptionTile(
|
_buildOptionTile(
|
||||||
'Schedule consultation',
|
'Consultation Centers',
|
||||||
Icons.medical_information_outlined,
|
Icons.medical_information_outlined,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
@ -159,21 +183,4 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _signOut() async {
|
|
||||||
try {
|
|
||||||
await _auth.signOut();
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context)
|
|
||||||
.pushReplacementNamed(RouteNames.scheduleConsultationScreen);
|
|
||||||
}
|
|
||||||
} 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,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
|
import 'package:medora/route/route_names.dart';
|
||||||
import '../../../route/route_names.dart';
|
|
||||||
|
|
||||||
class AchievementsScreen extends StatefulWidget {
|
class AchievementsScreen extends StatefulWidget {
|
||||||
final DoctorController controller;
|
final DoctorController controller;
|
||||||
@ -57,10 +56,6 @@ class _AchievementsScreenState extends State<AchievementsScreen> {
|
|||||||
_showError('Achievement must be at least 3 characters long');
|
_showError('Achievement must be at least 3 characters long');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[a-zA-Z0-9\s.,]+$').hasMatch(value)) {
|
|
||||||
_showError('Please enter valid achievement text');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
|
import 'package:medora/route/route_names.dart';
|
||||||
import '../../../route/route_names.dart';
|
|
||||||
|
|
||||||
class DoctorAddressScreen extends StatefulWidget {
|
class DoctorAddressScreen extends StatefulWidget {
|
||||||
final DoctorController? controller;
|
final DoctorController? controller;
|
||||||
@ -32,121 +31,81 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = widget.controller ?? DoctorController();
|
_controller = widget.controller ?? DoctorController();
|
||||||
|
_floorBuildingController = TextEditingController();
|
||||||
|
_streetController = TextEditingController();
|
||||||
|
_cityController = TextEditingController();
|
||||||
|
_stateController = TextEditingController();
|
||||||
|
_countryController = TextEditingController();
|
||||||
|
_postalCodeController = TextEditingController();
|
||||||
|
_addressTypeController = TextEditingController();
|
||||||
_loadSavedData();
|
_loadSavedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_floorBuildingController.dispose();
|
|
||||||
_streetController.dispose();
|
|
||||||
_cityController.dispose();
|
|
||||||
_stateController.dispose();
|
|
||||||
_countryController.dispose();
|
|
||||||
_postalCodeController.dispose();
|
|
||||||
_addressTypeController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadSavedData() {
|
void _loadSavedData() {
|
||||||
final doctor = _controller.model;
|
final doctor = _controller.model;
|
||||||
_floorBuildingController =
|
_floorBuildingController.text = doctor.floorBuilding ?? '';
|
||||||
TextEditingController(text: doctor.floorBuilding ?? '');
|
_streetController.text = doctor.street ?? '';
|
||||||
_streetController = TextEditingController(text: doctor.street ?? '');
|
_cityController.text = doctor.city ?? '';
|
||||||
_cityController = TextEditingController(text: doctor.city ?? '');
|
_stateController.text = doctor.state ?? '';
|
||||||
_stateController = TextEditingController(text: doctor.state ?? '');
|
_countryController.text = doctor.country ?? '';
|
||||||
_countryController = TextEditingController(text: doctor.country ?? '');
|
_postalCodeController.text = doctor.postalCode ?? '';
|
||||||
_postalCodeController =
|
|
||||||
TextEditingController(text: doctor.postalCode ?? '');
|
// Proper handling of address type
|
||||||
_addressTypeController =
|
if (doctor.addressType != null) {
|
||||||
TextEditingController(text: doctor.addressType ?? '');
|
final savedType = doctor.addressType!;
|
||||||
selectedAddressType = widget.controller?.model.addressType;
|
if (addressTypes.contains(savedType)) {
|
||||||
if (selectedAddressType != null &&
|
selectedAddressType = savedType;
|
||||||
!addressTypes.contains(selectedAddressType)) {
|
showCustomTypeField = false;
|
||||||
showCustomTypeField = true;
|
} else {
|
||||||
|
selectedAddressType = 'Others';
|
||||||
|
showCustomTypeField = true;
|
||||||
|
_addressTypeController.text = savedType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bool _validateAndProceed() {if (_formKey.currentState!.validate()) {
|
void _handleAddressTypeSelection(String type) {
|
||||||
// // Update the address model
|
setState(() {
|
||||||
// _controller.updateFloorBuilding(_floorBuildingController.text);
|
if (type == 'Others') {
|
||||||
// _controller.updateStreet(_streetController.text);
|
showCustomTypeField = true;
|
||||||
// _controller.updateCity(_cityController.text);
|
selectedAddressType = type;
|
||||||
// _controller.updateState(_stateController.text);
|
// Don't clear the custom field if it was previously set
|
||||||
// _controller.updateCountry(_countryController.text);
|
if (_addressTypeController.text.isEmpty) {
|
||||||
// _controller.updatePostalCode(_postalCodeController.text);
|
_controller.updateAddressType(''); // Clear the controller value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCustomTypeField = false;
|
||||||
|
selectedAddressType = type;
|
||||||
|
_addressTypeController.clear();
|
||||||
|
_controller.updateAddressType(
|
||||||
|
type); // Update controller with the selected type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// // Validate the address fields
|
|
||||||
// if (_areFieldsValid()) {
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
// const SnackBar(content: Text('Please fill in all required fields')),
|
|
||||||
// );
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
bool _validateAndProceed() {
|
bool _validateAndProceed() {
|
||||||
// if (!_formKey.currentState!.validate()) return false;
|
|
||||||
// if (selectedAddressType == null) {
|
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
||||||
// content: Text('Please select an address type'),
|
|
||||||
// backgroundColor: Colors.red,
|
|
||||||
// ));
|
|
||||||
// }
|
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Save all form data
|
||||||
_controller.updateFloorBuilding(_floorBuildingController.text);
|
_controller.updateFloorBuilding(_floorBuildingController.text);
|
||||||
_controller.updateStreet(_streetController.text);
|
_controller.updateStreet(_streetController.text);
|
||||||
_controller.updateCity(_cityController.text);
|
_controller.updateCity(_cityController.text);
|
||||||
_controller.updateState(_stateController.text);
|
_controller.updateState(_stateController.text);
|
||||||
_controller.updateCountry(_countryController.text);
|
_controller.updateCountry(_countryController.text);
|
||||||
_controller.updatePostalCode(_postalCodeController.text);
|
_controller.updatePostalCode(_postalCodeController.text);
|
||||||
_controller.updateAddressType(_addressTypeController.text);
|
|
||||||
|
// Handle address type saving
|
||||||
|
if (showCustomTypeField && _addressTypeController.text.isNotEmpty) {
|
||||||
|
_controller.updateAddressType(_addressTypeController.text);
|
||||||
|
} else if (selectedAddressType != null &&
|
||||||
|
selectedAddressType != 'Others') {
|
||||||
|
_controller.updateAddressType(selectedAddressType!);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _areFieldsValid() {
|
|
||||||
return _floorBuildingController.text.isNotEmpty &&
|
|
||||||
_streetController.text.isNotEmpty &&
|
|
||||||
_cityController.text.isNotEmpty &&
|
|
||||||
_stateController.text.isNotEmpty &&
|
|
||||||
_countryController.text.isNotEmpty &&
|
|
||||||
_postalCodeController.text.isNotEmpty &&
|
|
||||||
_addressTypeController.text.isNotEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bool _validateAndProceed() {
|
|
||||||
// if (!_formKey.currentState!.validate()) return false;
|
|
||||||
// if (selectedAddressType == null) {
|
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
// const SnackBar(
|
|
||||||
// content: Text('Please select an address type'),
|
|
||||||
// backgroundColor: Colors.red,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _handleAddressTypeSelection(String type) {
|
|
||||||
setState(() {
|
|
||||||
if (type == 'Others') {
|
|
||||||
showCustomTypeField = !showCustomTypeField;
|
|
||||||
if (!showCustomTypeField) {
|
|
||||||
_addressTypeController.clear();
|
|
||||||
selectedAddressType = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showCustomTypeField = false;
|
|
||||||
selectedAddressType = type;
|
|
||||||
widget.controller?.updateAddressType(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAddressTypeChips() {
|
Widget _buildAddressTypeChips() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -162,16 +121,13 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// Use the width of the layout to manage chip sizes
|
|
||||||
final chipMaxWidth = constraints.maxWidth;
|
final chipMaxWidth = constraints.maxWidth;
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 6, // Adjusted spacing to help fit in a single row
|
spacing: 6,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: addressTypes.map((type) {
|
children: addressTypes.map((type) {
|
||||||
final isSelected =
|
final isSelected = selectedAddressType == type;
|
||||||
!showCustomTypeField && selectedAddressType == type ||
|
|
||||||
(type == 'Others' && showCustomTypeField);
|
|
||||||
|
|
||||||
IconData icon;
|
IconData icon;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -198,10 +154,10 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
maxWidth:
|
maxWidth:
|
||||||
(chipMaxWidth - (addressTypes.length - 1) * 6) /
|
(chipMaxWidth - (addressTypes.length - 1) * 6) /
|
||||||
addressTypes.length,
|
addressTypes.length,
|
||||||
), // Calculate width to fit all chips within row
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12, // Reduced horizontal padding
|
horizontal: 12,
|
||||||
vertical: 8, // Reduced vertical padding
|
vertical: 8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
@ -215,14 +171,12 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon,
|
Icon(icon, color: Colors.blue, size: 16),
|
||||||
color: Colors.blue, size: 16), // Smaller icon
|
const SizedBox(width: 4),
|
||||||
const SizedBox(
|
|
||||||
width: 4), // Spacing between icon and text
|
|
||||||
Text(
|
Text(
|
||||||
type,
|
type,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14, // Reduced font size
|
fontSize: 14,
|
||||||
color: isSelected ? Colors.blue : Colors.black87,
|
color: isSelected ? Colors.blue : Colors.black87,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@ -239,33 +193,34 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
if (showCustomTypeField) ...[
|
if (showCustomTypeField) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _addressTypeController,
|
controller: _addressTypeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Custom Address Type',
|
labelText: 'Custom Address Type',
|
||||||
prefixIcon:
|
prefixIcon:
|
||||||
const Icon(Icons.edit_location_alt, color: Colors.blue),
|
const Icon(Icons.edit_location_alt, color: Colors.blue),
|
||||||
// filled: true,
|
contentPadding:
|
||||||
// fillColor: Colors.grey[100],
|
const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||||
contentPadding:
|
enabledBorder: OutlineInputBorder(
|
||||||
const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
borderRadius: BorderRadius.circular(8),
|
||||||
enabledBorder: OutlineInputBorder(
|
borderSide: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300, width: 1),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
focusedBorder: OutlineInputBorder(
|
||||||
if (showCustomTypeField && (value == null || value.isEmpty)) {
|
borderRadius: BorderRadius.circular(8),
|
||||||
return 'Please enter address type';
|
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
|
||||||
}
|
),
|
||||||
return null;
|
),
|
||||||
},
|
validator: (value) {
|
||||||
onChanged: (value) {
|
if (showCustomTypeField && (value == null || value.isEmpty)) {
|
||||||
|
return 'Please enter address type';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isNotEmpty) {
|
||||||
_controller.updateAddressType(value);
|
_controller.updateAddressType(value);
|
||||||
}),
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -434,7 +389,7 @@ class _DoctorAddressScreenState extends State<DoctorAddressScreen> {
|
|||||||
if (isMandatory && (value == null || value.isEmpty)) {
|
if (isMandatory && (value == null || value.isEmpty)) {
|
||||||
return '$label is required';
|
return '$label is required';
|
||||||
}
|
}
|
||||||
if (value != null && !RegExp(r'^[0-9]+$').hasMatch(value)) {
|
if (value != null && !RegExp(r'^(?!0{6})\d{6}$').hasMatch(value)) {
|
||||||
return 'Please enter numbers only';
|
return 'Please enter numbers only';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
import '../../../data/services/doctor_profile_service.dart';
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
import '../../../route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class DigitalSignatureScreen extends StatefulWidget {
|
class DigitalSignatureScreen extends StatefulWidget {
|
||||||
final DoctorController controller;
|
final DoctorController controller;
|
||||||
@ -59,12 +59,14 @@ class _DigitalSignatureScreenState extends State<DigitalSignatureScreen> {
|
|||||||
await DoctorProfileService.saveDoctorProfile(widget.controller);
|
await DoctorProfileService.saveDoctorProfile(widget.controller);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Doctor profile saved successfully!'),
|
content: Text('Doctor profile saved successfully!'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
Navigator.of(context).pushNamed(RouteNames.doctorLandingScreen);
|
Navigator.of(context).pushNamed(RouteNames.doctorLandingScreen);
|
||||||
} else {
|
} else {
|
||||||
_showError('Failed to save profile. Please try again.');
|
_showError('Failed to save profile. Please try again.');
|
||||||
|
|||||||
@ -23,7 +23,6 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
|||||||
String? _selectedTitle;
|
String? _selectedTitle;
|
||||||
|
|
||||||
final List<String> _titles = ['Mr', 'Mrs', 'Miss'];
|
final List<String> _titles = ['Mr', 'Mrs', 'Miss'];
|
||||||
final Map<String, String> _errors = {};
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -44,8 +43,6 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isEditing = false;
|
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@ -74,16 +71,10 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
|||||||
|
|
||||||
Future<void> _getImage(ImageSource source) async {
|
Future<void> _getImage(ImageSource source) async {
|
||||||
try {
|
try {
|
||||||
final XFile? image = await _picker.pickImage(
|
final XFile? image = await _picker.pickImage(source: source);
|
||||||
source: source,
|
|
||||||
imageQuality: 80,
|
|
||||||
maxWidth: 1000,
|
|
||||||
maxHeight: 1000,
|
|
||||||
);
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_image = File(image.path);
|
_image = File(image.path);
|
||||||
_isEditing = true;
|
|
||||||
});
|
});
|
||||||
_controller.updateProfileImage(image.path);
|
_controller.updateProfileImage(image.path);
|
||||||
}
|
}
|
||||||
@ -156,15 +147,14 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCombinedName(String name) {
|
|
||||||
String fullName = '$_selectedTitle$name';
|
|
||||||
_controller.updateSurName(fullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -242,7 +232,6 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildUniformField(
|
_buildUniformField(
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
|
|
||||||
icon: Icons.person,
|
icon: Icons.person,
|
||||||
child:
|
child:
|
||||||
Container(), // The child parameter is not used in this implementation
|
Container(), // The child parameter is not used in this implementation
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
|
import 'package:medora/data/services/doctor_profile_service.dart';
|
||||||
import '../../../route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class ExperienceScreen extends StatefulWidget {
|
class ExperienceScreen extends StatefulWidget {
|
||||||
final DoctorController controller;
|
final DoctorController controller;
|
||||||
@ -21,7 +21,6 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
|||||||
late final DoctorController _controller;
|
late final DoctorController _controller;
|
||||||
late TextEditingController _selectedExperience;
|
late TextEditingController _selectedExperience;
|
||||||
late TextEditingController _licenseController;
|
late TextEditingController _licenseController;
|
||||||
bool _isEditing = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -44,9 +43,7 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onFieldChanged() {
|
void _onFieldChanged() {
|
||||||
setState(() {
|
setState(() {});
|
||||||
_isEditing = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
@ -59,8 +56,17 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _validateAndProceed() {
|
Future<bool> _validateAndProceed() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Check for duplicate license number
|
||||||
|
bool isDuplicate = await DoctorProfileService.isLicenseNumberDuplicate(
|
||||||
|
_licenseController.text.trim());
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
_showError('This license number is already registered');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
_controller.updateYearsOfExperience(_selectedExperience.text);
|
_controller.updateYearsOfExperience(_selectedExperience.text);
|
||||||
_controller.updateLicenseNumber(_licenseController.text.trim());
|
_controller.updateLicenseNumber(_licenseController.text.trim());
|
||||||
return true;
|
return true;
|
||||||
@ -74,8 +80,8 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (_validateAndProceed()) {
|
if (await _validateAndProceed()) {
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
RouteNames.specialitiesScreeen,
|
RouteNames.specialitiesScreeen,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
import '../../../route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class ProfileDescriptionScreen extends StatefulWidget {
|
class ProfileDescriptionScreen extends StatefulWidget {
|
||||||
final DoctorController? controller;
|
final DoctorController? controller;
|
||||||
@ -19,7 +19,6 @@ class _ProfileDescriptionScreenState extends State<ProfileDescriptionScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
late DoctorController? _controller;
|
late DoctorController? _controller;
|
||||||
late TextEditingController _descriptionController;
|
late TextEditingController _descriptionController;
|
||||||
bool _isEditing = false;
|
|
||||||
final int _minDescriptionLength = 5;
|
final int _minDescriptionLength = 5;
|
||||||
final int _maxDescriptionLength = 500;
|
final int _maxDescriptionLength = 500;
|
||||||
|
|
||||||
@ -40,9 +39,7 @@ class _ProfileDescriptionScreenState extends State<ProfileDescriptionScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onDescriptionChanged() {
|
void _onDescriptionChanged() {
|
||||||
setState(() {
|
setState(() {});
|
||||||
_isEditing = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _validateDescription() {
|
bool _validateDescription() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
import '../../../route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class QualificationsScreen extends StatefulWidget {
|
class QualificationsScreen extends StatefulWidget {
|
||||||
final DoctorController? controller;
|
final DoctorController? controller;
|
||||||
@ -18,11 +18,9 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
late DoctorController _controller;
|
late DoctorController _controller;
|
||||||
late TextEditingController _qualificationsController;
|
late TextEditingController _qualificationsController;
|
||||||
late List<String> qualifications;
|
late List<String> qualifications;
|
||||||
bool _isEditing = false;
|
|
||||||
bool _showOthersField = false;
|
bool _showOthersField = false;
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// Predefined popular qualifications
|
|
||||||
final List<String> popularQualifications = [
|
final List<String> popularQualifications = [
|
||||||
'MBBS',
|
'MBBS',
|
||||||
'MD',
|
'MD',
|
||||||
@ -42,6 +40,7 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
_controller = widget.controller ?? DoctorController();
|
_controller = widget.controller ?? DoctorController();
|
||||||
_controller.model.qualifications ??= [];
|
_controller.model.qualifications ??= [];
|
||||||
_qualificationsController = TextEditingController();
|
_qualificationsController = TextEditingController();
|
||||||
|
// Create a deep copy of the qualifications list
|
||||||
qualifications = List<String>.from(_controller.model.qualifications ?? []);
|
qualifications = List<String>.from(_controller.model.qualifications ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +49,8 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
_showError('Please add at least one qualification');
|
_showError('Please add at least one qualification');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Sync the qualifications with the controller before navigation
|
||||||
|
_controller.model.qualifications = List<String>.from(qualifications);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,26 +83,10 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
|
|
||||||
if (_formKey.currentState!.validate() &&
|
if (_formKey.currentState!.validate() &&
|
||||||
_validateQualification(qualification)) {
|
_validateQualification(qualification)) {
|
||||||
// Check if qualification already exists (case-insensitive)
|
setState(() {
|
||||||
bool isDuplicate = qualifications
|
qualifications.add(qualification);
|
||||||
.any((q) => q.toLowerCase() == qualification.toLowerCase());
|
_qualificationsController.clear();
|
||||||
|
});
|
||||||
if (!isDuplicate) {
|
|
||||||
setState(() {
|
|
||||||
qualifications.add(qualification);
|
|
||||||
_isEditing = true;
|
|
||||||
_qualificationsController.clear();
|
|
||||||
});
|
|
||||||
_controller.addQualification(qualification);
|
|
||||||
} else {
|
|
||||||
// Show error message for duplicate qualification
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('This qualification has already been added'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,23 +99,14 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
|
|
||||||
if (qualifications.contains(qualification)) {
|
if (qualifications.contains(qualification)) {
|
||||||
qualifications.remove(qualification);
|
qualifications.remove(qualification);
|
||||||
_controller.removeQualification(qualification);
|
|
||||||
} else {
|
} else {
|
||||||
// Check if qualification already exists (case-insensitive)
|
|
||||||
bool isDuplicate = qualifications
|
bool isDuplicate = qualifications
|
||||||
.any((q) => q.toLowerCase() == qualification.toLowerCase());
|
.any((q) => q.toLowerCase() == qualification.toLowerCase());
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
qualifications.add(qualification);
|
qualifications.add(qualification);
|
||||||
_controller.addQualification(qualification);
|
|
||||||
} else {
|
} else {
|
||||||
// Show error message for duplicate qualification
|
_showError('This qualification has already been added');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('This qualification has already been added'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -138,10 +114,9 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
|
|
||||||
void _removeQualification(int index) {
|
void _removeQualification(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
// Remove the qualification at the specified index
|
||||||
qualifications.removeAt(index);
|
qualifications.removeAt(index);
|
||||||
_isEditing = true;
|
|
||||||
});
|
});
|
||||||
_controller.removeQualification(_qualificationsController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
@ -156,175 +131,183 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
// ignore: deprecated_member_use
|
||||||
appBar: AppBar(
|
return WillPopScope(
|
||||||
actions: [
|
onWillPop: () async {
|
||||||
IconButton(
|
// Sync qualifications with controller before popping
|
||||||
onPressed: () {
|
_controller.model.qualifications = List<String>.from(qualifications);
|
||||||
if (_validateBeforeNextPage()) {
|
return true;
|
||||||
Navigator.pushNamed(
|
},
|
||||||
context,
|
child: Scaffold(
|
||||||
RouteNames.doctorAddressScreen,
|
appBar: AppBar(
|
||||||
arguments: _controller,
|
actions: [
|
||||||
);
|
IconButton(
|
||||||
}
|
onPressed: () {
|
||||||
},
|
if (_validateBeforeNextPage()) {
|
||||||
icon: const Icon(Icons.arrow_forward),
|
Navigator.pushNamed(
|
||||||
),
|
context,
|
||||||
],
|
RouteNames.doctorAddressScreen,
|
||||||
title: const Text('Qualifications'),
|
arguments: _controller,
|
||||||
),
|
);
|
||||||
body: SingleChildScrollView(
|
}
|
||||||
child: Padding(
|
},
|
||||||
padding: const EdgeInsets.all(16.0),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
spreadRadius: 5,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Form(
|
],
|
||||||
key: _formKey,
|
title: const Text('Qualifications'),
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
body: SingleChildScrollView(
|
||||||
children: [
|
child: Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: const EdgeInsets.all(20),
|
child: Container(
|
||||||
child: GridView.builder(
|
decoration: BoxDecoration(
|
||||||
shrinkWrap: true,
|
color: Colors.white,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
borderRadius: BorderRadius.circular(16),
|
||||||
gridDelegate:
|
boxShadow: [
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
BoxShadow(
|
||||||
crossAxisCount: 3,
|
color: Colors.grey.withOpacity(0.1),
|
||||||
childAspectRatio: 2.5,
|
spreadRadius: 5,
|
||||||
crossAxisSpacing: 10,
|
blurRadius: 10,
|
||||||
mainAxisSpacing: 10,
|
offset: const Offset(0, 3),
|
||||||
),
|
),
|
||||||
itemCount: popularQualifications.length,
|
],
|
||||||
itemBuilder: (context, index) {
|
),
|
||||||
final qualification = popularQualifications[index];
|
child: Form(
|
||||||
final isSelected = qualification != 'Others' &&
|
key: _formKey,
|
||||||
qualifications.contains(qualification);
|
child: Column(
|
||||||
final isOthers = qualification == 'Others';
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
childAspectRatio: 2.5,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
),
|
||||||
|
itemCount: popularQualifications.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final qualification = popularQualifications[index];
|
||||||
|
final isSelected = qualification != 'Others' &&
|
||||||
|
qualifications.contains(qualification);
|
||||||
|
final isOthers = qualification == 'Others';
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _toggleQualification(qualification),
|
onTap: () => _toggleQualification(qualification),
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
|
||||||
isSelected || (isOthers && _showOthersField)
|
|
||||||
? Colors.blue.withOpacity(0.2)
|
|
||||||
: Colors.grey.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ||
|
color: isSelected ||
|
||||||
(isOthers && _showOthersField)
|
(isOthers && _showOthersField)
|
||||||
? Colors.blue
|
? Colors.blue.withOpacity(0.2)
|
||||||
: Colors.transparent,
|
: Colors.grey.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ||
|
||||||
|
(isOthers && _showOthersField)
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
qualification,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected ||
|
||||||
|
(isOthers && _showOthersField)
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
),
|
||||||
child: Text(
|
);
|
||||||
qualification,
|
},
|
||||||
style: TextStyle(
|
),
|
||||||
color: isSelected ||
|
),
|
||||||
(isOthers && _showOthersField)
|
if (_showOthersField) ...[
|
||||||
? Colors.blue
|
Container(
|
||||||
: Colors.black87,
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.blue.shade100),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _qualificationsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Enter your qualification',
|
||||||
|
border: InputBorder.none,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.add_circle_outline,
|
||||||
|
color: Colors.blue),
|
||||||
|
onPressed: _newQualification,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onFieldSubmitted: (_) => _newQualification(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
if (qualifications.isNotEmpty) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Text(
|
||||||
|
'Selected Qualifications',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: qualifications.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
qualifications[index],
|
||||||
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () => _removeQualification(index),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_showOthersField) ...[
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.blue.shade100),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
child: TextFormField(
|
|
||||||
controller: _qualificationsController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Enter your qualification',
|
|
||||||
border: InputBorder.none,
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: const Icon(Icons.add_circle_outline,
|
|
||||||
color: Colors.blue),
|
|
||||||
onPressed: _newQualification,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onFieldSubmitted: (_) => _newQualification(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
if (qualifications.isNotEmpty) ...[
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Text(
|
|
||||||
'Selected Qualifications',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: qualifications.length,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
title: Text(
|
|
||||||
qualifications[index],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
onPressed: () => _removeQualification(index),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:medora/controllers/doctor_controller.dart';
|
import 'package:medora/controllers/doctor_controller.dart';
|
||||||
import '../../../route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
|
||||||
class SpecialitiesScreen extends StatefulWidget {
|
class SpecialitiesScreen extends StatefulWidget {
|
||||||
final DoctorController controller;
|
final DoctorController controller;
|
||||||
@ -16,77 +16,223 @@ class SpecialitiesScreen extends StatefulWidget {
|
|||||||
class _SpecialitiesScreenState extends State<SpecialitiesScreen> {
|
class _SpecialitiesScreenState extends State<SpecialitiesScreen> {
|
||||||
String? selectedSpeciality;
|
String? selectedSpeciality;
|
||||||
late final DoctorController _controller;
|
late final DoctorController _controller;
|
||||||
late TextEditingController _speciality;
|
|
||||||
bool _isEditing = false;
|
|
||||||
|
|
||||||
final List<Map<String, dynamic>> specialities = [
|
final List<Map<String, dynamic>> specialities = [
|
||||||
{
|
{
|
||||||
'icon': Icons.child_care,
|
'icon': Icons.child_care,
|
||||||
'label': 'Pediatric',
|
'label': 'Pediatric',
|
||||||
'value': 'pediatric',
|
'value': 'Pediatric',
|
||||||
'description': 'Specialist in child healthcare',
|
'description': 'Medical care for infants, children, and adolescents',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.medical_services,
|
'icon': Icons.medical_services,
|
||||||
'label': 'Casual',
|
'label': 'General Medicine',
|
||||||
'value': 'casual',
|
'value': 'General Medicine',
|
||||||
'description': 'General healthcare provider',
|
'description':
|
||||||
|
'Primary healthcare for adults and general medical conditions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.coronavirus,
|
'icon': Icons.family_restroom,
|
||||||
'label': 'Corona',
|
'label': 'Family Medicine',
|
||||||
'value': 'corona',
|
'value': 'Family Medicine',
|
||||||
'description': 'COVID-19 specialist',
|
'description': 'Comprehensive healthcare for families and individuals',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.favorite,
|
||||||
|
'label': 'Cardiologist',
|
||||||
|
'value': 'Cardiologist',
|
||||||
|
'description': 'Diagnosis and treatment of heart conditions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.psychology,
|
||||||
|
'label': 'Neurology',
|
||||||
|
'value': 'Neurology',
|
||||||
|
'description': 'Treatment of nervous system disorders',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.local_hospital,
|
||||||
|
'label': 'Gastroenterology',
|
||||||
|
'value': 'Gastroenterology',
|
||||||
|
'description': 'Digestive system disorders and treatment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.face,
|
||||||
|
'label': 'Dermatologist',
|
||||||
|
'value': 'Dermatologist',
|
||||||
|
'description': 'Skin, hair, and nail conditions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.wheelchair_pickup,
|
||||||
|
'label': 'Orthopedic',
|
||||||
|
'value': 'Orthopedic',
|
||||||
|
'description': 'Musculoskeletal system and injury treatment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.remove_red_eye,
|
||||||
|
'label': 'Ophthalmology',
|
||||||
|
'value': 'Ophthalmology',
|
||||||
|
'description': 'Eye care and vision treatment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.hearing,
|
||||||
|
'label': 'ENT',
|
||||||
|
'value': 'ENT',
|
||||||
|
'description': 'Ear, nose, and throat specialist',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.psychology_outlined,
|
||||||
|
'label': 'Psychiatry',
|
||||||
|
'value': 'Psychiatry',
|
||||||
|
'description': 'Mental health and behavioral disorders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.pregnant_woman,
|
'icon': Icons.pregnant_woman,
|
||||||
'label': 'Gynecology',
|
'label': 'Gynecology',
|
||||||
'value': 'gynecology',
|
'value': 'Gynecology',
|
||||||
'description': 'Women\'s health specialist',
|
'description': "Women's health and reproductive care",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.medical_services_outlined,
|
'icon': Icons.water_drop,
|
||||||
'label': 'Orthopedic',
|
'label': 'Urology',
|
||||||
'value': 'orthopedic',
|
'value': 'Urology',
|
||||||
'description': 'Bone and joint specialist',
|
'description': 'Urinary tract and male reproductive health',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.remove_red_eye,
|
'icon': Icons.biotech,
|
||||||
'label': 'Eye',
|
'label': 'Endocrinology',
|
||||||
'value': 'eye',
|
'value': 'Endocrinology',
|
||||||
'description': 'Eye care specialist',
|
'description': 'Hormone and metabolic disorders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.psychology,
|
'icon': Icons.bloodtype,
|
||||||
'label': 'Psychiatrist',
|
'label': 'Oncology',
|
||||||
'value': 'psychiatrist',
|
'value': 'Oncology',
|
||||||
'description': 'Mental health specialist',
|
'description': 'Cancer diagnosis and treatment',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.medical_information,
|
'icon': Icons.accessibility,
|
||||||
|
'label': 'Rheumatology',
|
||||||
|
'value': 'Rheumatology',
|
||||||
|
'description': 'Arthritis and autoimmune conditions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.air,
|
||||||
|
'label': 'Pulmonology',
|
||||||
|
'value': 'Pulmonology',
|
||||||
|
'description': 'Respiratory system disorders',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.water,
|
||||||
|
'label': 'Nephrology',
|
||||||
|
'value': 'Nephrology',
|
||||||
|
'description': 'Kidney diseases and disorders',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.cleaning_services,
|
||||||
'label': 'Dentistry',
|
'label': 'Dentistry',
|
||||||
'value': 'dental',
|
'value': 'Dentistry',
|
||||||
'description': 'Dental care specialist',
|
'description': 'Oral health and dental care',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.person,
|
'icon': Icons.accessibility_new,
|
||||||
'label': 'General Medicine',
|
'label': 'Physical Therapy',
|
||||||
'value': 'general',
|
'value': 'Physical Therapy',
|
||||||
'description': 'General practitioner',
|
'description': 'Rehabilitation and physical medicine',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.sports,
|
||||||
|
'label': 'Sports Medicine',
|
||||||
|
'value': 'Sports Medicine',
|
||||||
|
'description': 'Athletic injuries and performance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.sick,
|
||||||
|
'label': 'Allergy & Immunology',
|
||||||
|
'value': 'Allergy & Immunology',
|
||||||
|
'description': 'Allergies and immune system disorders',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.healing,
|
||||||
|
'label': 'Pain Management',
|
||||||
|
'value': 'Pain Management',
|
||||||
|
'description': 'Chronic pain treatment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.bedtime,
|
||||||
|
'label': 'Sleep Medicine',
|
||||||
|
'value': 'Sleep Medicine',
|
||||||
|
'description': 'Sleep disorders and treatment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'icon': Icons.elderly,
|
||||||
|
'label': 'Geriatrics',
|
||||||
|
'value': 'Geriatrics',
|
||||||
|
'description': 'Healthcare for elderly patients',
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.child_care,
|
||||||
|
// 'label': 'Pediatric',
|
||||||
|
// 'value': 'Pediatric',
|
||||||
|
// 'description': 'Specialist in child healthcare',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.medical_services,
|
||||||
|
// 'label': 'Casual',
|
||||||
|
// 'value': 'casual',
|
||||||
|
// 'description': 'General healthcare provider',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.coronavirus,
|
||||||
|
// 'label': 'Corona',
|
||||||
|
// 'value': 'corona',
|
||||||
|
// 'description': 'COVID-19 specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.pregnant_woman,
|
||||||
|
// 'label': 'Gynecology',
|
||||||
|
// 'value': 'gynecology',
|
||||||
|
// 'description': 'Women\'s health specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.medical_services_outlined,
|
||||||
|
// 'label': 'Orthopedic',
|
||||||
|
// 'value': 'orthopedic',
|
||||||
|
// 'description': 'Bone and joint specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.remove_red_eye,
|
||||||
|
// 'label': 'Eye',
|
||||||
|
// 'value': 'eye',
|
||||||
|
// 'description': 'Eye care specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.psychology,
|
||||||
|
// 'label': 'Psychiatrist',
|
||||||
|
// 'value': 'psychiatrist',
|
||||||
|
// 'description': 'Mental health specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.medical_information,
|
||||||
|
// 'label': 'Dentistry',
|
||||||
|
// 'value': 'dental',
|
||||||
|
// 'description': 'Dental care specialist',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// 'icon': Icons.person,
|
||||||
|
// 'label': 'General Medicine',
|
||||||
|
// 'value': 'general',
|
||||||
|
// 'description': 'General practitioner',
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = widget.controller ?? DoctorController();
|
_controller = widget.controller;
|
||||||
_loadSavedData();
|
_loadSavedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadSavedData() {
|
void _loadSavedData() {}
|
||||||
final doctor = _controller.model;
|
|
||||||
_speciality = TextEditingController(text: doctor.speciality ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -163,7 +309,6 @@ class _SpecialitiesScreenState extends State<SpecialitiesScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
// _speciality = specialty['value'];
|
// _speciality = specialty['value'];
|
||||||
selectedSpeciality = specialty['value'];
|
selectedSpeciality = specialty['value'];
|
||||||
_isEditing = true;
|
|
||||||
widget.controller.updateSpeciality(selectedSpeciality!);
|
widget.controller.updateSpeciality(selectedSpeciality!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user