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:
Jipson George 2024-11-14 11:00:28 +00:00 committed by Benoy Bose
parent 42543367a4
commit 520c9b6e44
22 changed files with 1253 additions and 660 deletions

View File

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

View File

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

View File

@ -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'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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