From 520c9b6e446101ab79a2e6eb0d83e67a49f0a294 Mon Sep 17 00:00:00 2001 From: Jipson George Date: Thu, 14 Nov 2024 11:00:28 +0000 Subject: [PATCH] feature/medora-125 (#9) fixed bugs Co-authored-by: DhanshCOSQ Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/telemednet/pulls/9 Reviewed-by: Benoy Bose Co-authored-by: Jipson George Co-committed-by: Jipson George --- .../consultation_center_controller.dart | 2 +- lib/controllers/doctor_controller.dart | 4 +- lib/data/models/doctor.dart | 4 + .../services/consultation_center_service.dart | 2 +- lib/data/services/doctor_profile_service.dart | 243 +++++++++--- lib/route/routes.dart | 2 +- .../authentication/sign_up_screen.dart | 157 +++++++- .../business_center_screen.dart | 43 +-- .../center_fee_and_duration_screen.dart | 11 +- ....dart => consultation_centers_screen.dart} | 18 +- .../consultation_time_slot_screen.dart | 85 ++-- .../doctor_dashboard_home_screen.dart | 326 ++++++++++++++-- .../doctor_personal_profile_screen.dart | 55 ++- .../doctor_services_menu_screen.dart | 87 +++-- .../achivements_screen.dart | 7 +- .../address_screen.dart | 221 +++++------ .../digital_signature_screen.dart | 6 +- .../doctor_profile_screen.dart | 21 +- .../experience_screen.dart | 24 +- .../profile_description_screen.dart | 7 +- .../qualifications_screen.dart | 365 +++++++++--------- .../specialities_selection_screen.dart | 223 +++++++++-- 22 files changed, 1253 insertions(+), 660 deletions(-) rename lib/screens/doctor_screen/doctor_consultation_schedule/{consultation_schedule.dart => consultation_centers_screen.dart} (95%) diff --git a/lib/controllers/consultation_center_controller.dart b/lib/controllers/consultation_center_controller.dart index eeaa2ec..b64c000 100644 --- a/lib/controllers/consultation_center_controller.dart +++ b/lib/controllers/consultation_center_controller.dart @@ -1,4 +1,4 @@ -import '../data/models/consultation_center.dart'; +import 'package:medora/data/models/consultation_center.dart'; class ConsultationCenterController { final ConsultationCenter model; diff --git a/lib/controllers/doctor_controller.dart b/lib/controllers/doctor_controller.dart index 1d0dca8..90366d7 100644 --- a/lib/controllers/doctor_controller.dart +++ b/lib/controllers/doctor_controller.dart @@ -97,11 +97,11 @@ class DoctorController { } void addQualification(String qualification) { - model.qualifications!.add(qualification.trim()); + model.qualifications!.add(qualification); } void removeQualification(String qualification) { - model.qualifications!.remove(qualification.trim()); + model.qualifications!.remove(qualification); } void updateFloorBuilding(String floorBuilding) { diff --git a/lib/data/models/doctor.dart b/lib/data/models/doctor.dart index 1fc5f23..f0b357d 100644 --- a/lib/data/models/doctor.dart +++ b/lib/data/models/doctor.dart @@ -2,6 +2,7 @@ class Doctor { List? achievements; String? uid; String? profileImage; + String? profileImageUrl; String? speciality; String? yearsOfExperience; String? licenseNumber; @@ -23,6 +24,7 @@ class Doctor { Doctor({ this.addressType, this.achievements, + this.profileImageUrl, this.profileImage, // Initialize with empty list this.speciality, this.yearsOfExperience, @@ -44,6 +46,7 @@ class Doctor { }); Map toJson() => { + 'profileImagePath': profileImageUrl, 'profileImage': profileImage, 'achievements': achievements, 'speciality': speciality, @@ -67,6 +70,7 @@ class Doctor { static Doctor fromJson(Map json) => Doctor( achievements: List.from(json['achievements'] ?? []), + profileImageUrl: json['profileImageUrl'], profileImage: json['profileImage'], speciality: json['speciality'], yearsOfExperience: json['yearsOfExperience'], diff --git a/lib/data/services/consultation_center_service.dart b/lib/data/services/consultation_center_service.dart index d019ce8..1827454 100644 --- a/lib/data/services/consultation_center_service.dart +++ b/lib/data/services/consultation_center_service.dart @@ -2,7 +2,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:medora/controllers/consultation_center_controller.dart'; -import '../models/consultation_center.dart'; +import 'package:medora/data/models/consultation_center.dart'; class ConsultationCenterService { static final String consultationCenterCollectionName = diff --git a/lib/data/services/doctor_profile_service.dart b/lib/data/services/doctor_profile_service.dart index e193d76..35c3112 100644 --- a/lib/data/services/doctor_profile_service.dart +++ b/lib/data/services/doctor_profile_service.dart @@ -1,13 +1,184 @@ +import 'dart:io'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:medora/controllers/doctor_controller.dart'; import 'package:medora/data/models/doctor.dart'; +import 'package:path/path.dart' as path; class DoctorProfileService { static final String doctorProfileCollectionName = dotenv.env['DOCTOR_PROFILE_COLLECTION_NAME']!; static final FirebaseFirestore db = FirebaseFirestore.instance; + static final FirebaseStorage storage = FirebaseStorage.instanceFor( + bucket: dotenv.env['FIREBASE_STORAGE_BUCKET']!); + + static Future 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 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 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 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 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; + 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 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 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; + 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 getDoctorProfile() async { try { @@ -22,7 +193,7 @@ class DoctorProfileService { await db.collection(doctorProfileCollectionName).doc(uid).get(); if (!doc.exists) { - print('No patient profile found for this user'); + print('No doctor profile found for this user'); return null; } @@ -34,72 +205,16 @@ class DoctorProfileService { } } - static Future saveDoctorProfile(DoctorController controller) async { + static Future isLicenseNumberDuplicate(String licenseNumber) 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 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 doctorJson = doctorData.toJson(); - doctorJson['updatedAt'] = FieldValue.serverTimestamp(); - - await db + final querySnapshot = await db .collection(doctorProfileCollectionName) - .doc(uid) - .update(doctorJson); - print('Doctor profile updated successfully'); - return true; + .where('licenseNumber', isEqualTo: licenseNumber) + .get(); + + return querySnapshot.docs.isNotEmpty; } catch (e) { - print('Error updating doctor profile: $e'); - return false; - } - } - - static Future deleteDoctorProfile() async { - try { - final User? user = FirebaseAuth.instance.currentUser; - if (user == null) { - print('No user logged in'); - return false; - } - - final String uid = user.uid; - - await db.collection(doctorProfileCollectionName).doc(uid).delete(); - print('Doctor profile deleted successfully'); - return true; - } catch (e) { - print('Error deleting doctor profile: $e'); + print('Error checking license number: $e'); return false; } } diff --git a/lib/route/routes.dart b/lib/route/routes.dart index 83e7bae..22791d4 100644 --- a/lib/route/routes.dart +++ b/lib/route/routes.dart @@ -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/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_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_dashboard/doctor_dashboard_home_screen.dart'; import 'package:medora/screens/doctor_screen/doctor_dashboard/doctor_dashboard_screen.dart'; diff --git a/lib/screens/authentication/sign_up_screen.dart b/lib/screens/authentication/sign_up_screen.dart index b8cf0d1..a464803 100644 --- a/lib/screens/authentication/sign_up_screen.dart +++ b/lib/screens/authentication/sign_up_screen.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:intl_phone_field/intl_phone_field.dart'; import 'package:medora/data/services/data_service.dart'; @@ -24,6 +26,123 @@ class _SignUpScreenState extends State { bool _isLoading = false; 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 _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 previousRow = List.generate(b.length + 1, (i) => i); + List currentRow = List.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 temp = previousRow; + previousRow = currentRow; + currentRow = temp; + } + + return previousRow[b.length]; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -51,6 +170,7 @@ class _SignUpScreenState extends State { children: [ TextFormField( controller: _emailController, + focusNode: _emailFocusNode, decoration: InputDecoration( labelText: 'Email', border: OutlineInputBorder( @@ -65,22 +185,25 @@ class _SignUpScreenState extends State { ), prefixIcon: const Icon(Icons.email_outlined, color: Colors.blue), + errorMaxLines: 2, ), keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value?.isEmpty ?? true) { - return 'Please enter your email'; + textInputAction: TextInputAction.next, + onFieldSubmitted: (_) { + _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), TextFormField( controller: _passwordController, + focusNode: _passwordFocusNode, decoration: InputDecoration( labelText: 'Password', border: OutlineInputBorder( @@ -106,16 +229,15 @@ class _SignUpScreenState extends State { onPressed: () => setState( () => _obscurePassword = !_obscurePassword), ), + errorMaxLines: 2, ), obscureText: _obscurePassword, - validator: (value) { - if (value?.isEmpty ?? true) { - return 'Please enter your password'; + validator: _validatePassword, + onChanged: (value) { + // 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), @@ -140,7 +262,7 @@ class _SignUpScreenState extends State { }, validator: (phone) { if (phone?.completeNumber.isEmpty ?? true) { - return 'Please enter your phone number'; + return 'Phone number is required'; } return null; }, @@ -165,6 +287,7 @@ class _SignUpScreenState extends State { Future _handleSignUp() async { if (_formKey.currentState == null || !_formKey.currentState!.validate()) { + _showErrorSnackBar('Please fix the errors in the form'); return; } @@ -222,6 +345,8 @@ class _SignUpScreenState extends State { void dispose() { _emailController.dispose(); _passwordController.dispose(); + _emailFocusNode.dispose(); + _passwordFocusNode.dispose(); super.dispose(); } } diff --git a/lib/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart b/lib/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart index a8d7e11..f643c05 100644 --- a/lib/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart +++ b/lib/screens/doctor_screen/doctor_consultation_schedule/business_center_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/consultation_center_controller.dart'; - -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class BusinessCenterScreen extends StatefulWidget { final ConsultationCenterController controller; @@ -59,7 +58,7 @@ class _ConsultationCenterScreenState extends State { TextEditingController(text: center.postalCode ?? ''); _addressTypeController = TextEditingController(text: center.addressType ?? ''); - selectedAddressType = widget.controller?.model.addressType; + selectedAddressType = widget.controller.model.addressType; if (selectedAddressType != null && !addressTypes.contains(selectedAddressType)) { showCustomTypeField = true; @@ -81,32 +80,6 @@ class _ConsultationCenterScreenState extends State { 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 Widget build(BuildContext context) { return Scaffold( @@ -151,35 +124,35 @@ class _ConsultationCenterScreenState extends State { label: 'Floor, Building', controller: _floorBuildingController, onChanged: (value) => - widget.controller!.updateFloorBuilding(value), + widget.controller.updateFloorBuilding(value), icon: Icons.apartment, isMandatory: true, ), _buildTextField( label: 'Street or Road', controller: _streetController, - onChanged: (value) => widget.controller!.updateStreet(value), + onChanged: (value) => widget.controller.updateStreet(value), icon: Icons.streetview, isMandatory: true, ), _buildTextField( label: 'City', controller: _cityController, - onChanged: (value) => widget.controller!.updateCity(value), + onChanged: (value) => widget.controller.updateCity(value), icon: Icons.location_city, isMandatory: true, ), _buildTextField( label: 'State', controller: _stateController, - onChanged: (value) => widget.controller!.updateState(value), + onChanged: (value) => widget.controller.updateState(value), icon: Icons.map, isMandatory: true, ), _buildTextField( label: 'Country', controller: _countryController, - onChanged: (value) => widget.controller!.updateCountry(value), + onChanged: (value) => widget.controller.updateCountry(value), icon: Icons.flag, isMandatory: true, ), @@ -187,7 +160,7 @@ class _ConsultationCenterScreenState extends State { label: 'Postal Code', controller: _postalCodeController, onChanged: (value) => - widget.controller!.updatePostalCode(value), + widget.controller.updatePostalCode(value), icon: Icons.mail, isMandatory: true, ), diff --git a/lib/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart b/lib/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart index e1fa206..f3ae5bb 100644 --- a/lib/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart +++ b/lib/screens/doctor_screen/doctor_consultation_schedule/center_fee_and_duration_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/services.dart'; import 'package:medora/controllers/consultation_center_controller.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 '../../../route/route_names.dart'; class CenterFeeAndDurationScreen extends StatefulWidget { final ConsultationCenterController controller; @@ -25,7 +25,6 @@ class CenterFeeAndDurationScreenState late final ConsultationCenterController _controller; late TextEditingController _averageDuration; late TextEditingController _consultationFee; - bool _isEditing = false; @override void initState() { @@ -46,9 +45,7 @@ class CenterFeeAndDurationScreenState } void _onFieldChanged() { - setState(() { - _isEditing = true; - }); + setState(() {}); } void _showError(String message) { @@ -85,12 +82,14 @@ class CenterFeeAndDurationScreenState await ConsultationCenterService.saveConsultationCenters(controllers); if (success) { + // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Doctor consultation saved successfully!'), backgroundColor: Colors.green, ), ); + // ignore: use_build_context_synchronously Navigator.of(context) .pushReplacementNamed(RouteNames.scheduleConsultationScreen); } else { @@ -146,6 +145,7 @@ class CenterFeeAndDurationScreenState TextFormField( controller: _consultationFee, keyboardType: TextInputType.number, + textAlign: TextAlign.end, inputFormatters: [FilteringTextInputFormatter.digitsOnly], validator: (value) { if (value == null || value.isEmpty) { @@ -191,6 +191,7 @@ class CenterFeeAndDurationScreenState const SizedBox(height: 24), TextFormField( controller: _averageDuration, + textAlign: TextAlign.end, keyboardType: TextInputType.number, validator: (value) { if (value == null || value.isEmpty) { diff --git a/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_schedule.dart b/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_centers_screen.dart similarity index 95% rename from lib/screens/doctor_screen/doctor_consultation_schedule/consultation_schedule.dart rename to lib/screens/doctor_screen/doctor_consultation_schedule/consultation_centers_screen.dart index 1b57e00..7b2ebc2 100644 --- a/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_schedule.dart +++ b/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_centers_screen.dart @@ -1,11 +1,10 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.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/services/consultation_center_service.dart'; - -import '../../../common/custom_style.dart'; -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class ScheduleConsultationScreen extends StatefulWidget { const ScheduleConsultationScreen({super.key}); @@ -64,7 +63,7 @@ class ScheduleConsultationScreenState context, RouteNames.doctorDashbordScreen), ), title: Text( - 'Schedule Consultation', + 'Consultation Centers', style: GoogleFonts.poppins( fontWeight: FontWeight.w600, ), @@ -114,7 +113,7 @@ class ScheduleConsultationScreenState color: Theme.of(context).colorScheme.onSurface, ), ), - trailing: Container( + trailing: SizedBox( width: 80, // Increased width to accommodate contents child: Row( mainAxisSize: @@ -270,10 +269,17 @@ class ScheduleConsultationScreenState size: 18, ), onPressed: () { + // Pass the existing center data to maintain state + // final consultationController = + // ConsultationCenterController( + // center); + // Navigator.pushNamed( // context, // RouteNames - // .ConsultationDayScreen, + // .consultationDayScreen, + // arguments: + // consultationController, // ); }, ), diff --git a/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart b/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart index 6feb335..e3ba36e 100644 --- a/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart +++ b/lib/screens/doctor_screen/doctor_consultation_schedule/consultation_time_slot_screen.dart @@ -52,6 +52,15 @@ class _ConsultationTimeSlotScreenState '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 { TimeOfDay? startTime = await showTimePicker( context: context, @@ -66,74 +75,57 @@ class _ConsultationTimeSlotScreenState if (endTime != null) { final slot = TimeSlot( - startTime: - '${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')}', - endTime: - '${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}', + startTime: formatTime(startTime), + endTime: formatTime(endTime), ); if (mounted) { setState(() { widget.controller.addTimeSlot(widget.selectedDay, slot); - // Update local schedule currentSchedule = - widget.controller.model.weeklySchedule?.firstWhere( - (schedule) => schedule.day == widget.selectedDay, - orElse: () => AvailabilitySchedule( - day: widget.selectedDay, - timeSlots: [], - ), - ) ?? - AvailabilitySchedule( - day: widget.selectedDay, - timeSlots: [], - ); + widget.controller.model.weeklySchedule!.firstWhere( + (schedule) => schedule.day == widget.selectedDay, + orElse: () => AvailabilitySchedule( + day: widget.selectedDay, + timeSlots: [], + ), + ); }); - - // Debug print - print('Added time slot: $slot'); - print('Updated schedule: ${currentSchedule.timeSlots}'); } } } } void _editTimeSlot(TimeSlot currentSlot) async { - final currentStart = currentSlot.startTime!.split(':'); - final currentEnd = currentSlot.endTime!.split(':'); + final currentStart = currentSlot.startTime!.split(' '); + currentSlot.endTime!.split(' '); - TimeOfDay? startTime = await showTimePicker( - context: context, - initialTime: TimeOfDay( - hour: int.parse(currentStart[0]), - minute: int.parse(currentStart[1]), - ), + final startTime = TimeOfDay( + hour: int.parse(currentStart[0].split(':')[0]), + minute: int.parse(currentStart[0].split(':')[1]), ); - if (startTime != null) { - TimeOfDay? endTime = await showTimePicker( + TimeOfDay? newStartTime = await showTimePicker( + context: context, + initialTime: startTime, + ); + + if (newStartTime != null) { + TimeOfDay? newEndTime = await showTimePicker( context: context, - initialTime: TimeOfDay( - hour: int.parse(currentEnd[0]), - minute: int.parse(currentEnd[1]), - ), + initialTime: newStartTime, ); - if (endTime != null && mounted) { + if (newEndTime != null && mounted) { setState(() { - // Remove old slot widget.controller.removeTimeSlot(widget.selectedDay, currentSlot); - // Add new slot final newSlot = TimeSlot( - startTime: - '${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')}', - endTime: - '${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}', + startTime: formatTime(newStartTime), + endTime: formatTime(newEndTime), ); widget.controller.addTimeSlot(widget.selectedDay, newSlot); - // Update local schedule currentSchedule = widget.controller.model.weeklySchedule!.firstWhere( (schedule) => schedule.day == widget.selectedDay, 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( '${slot.startTime} - ${slot.endTime}', style: const TextStyle( - fontSize: 20, + fontSize: 13.5, fontWeight: FontWeight.w600, color: Colors.black87, ), @@ -325,7 +314,7 @@ class _ConsultationTimeSlotScreenState IconButton( icon: const Icon( Icons.edit_outlined, - size: 30, + size: 26, color: Colors.blue, ), onPressed: () => _editTimeSlot(slot), @@ -333,7 +322,7 @@ class _ConsultationTimeSlotScreenState IconButton( icon: const Icon( Icons.delete_outline, - size: 30, + size: 26, color: Colors.red, ), onPressed: () => _deleteTimeSlot(slot), diff --git a/lib/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart b/lib/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart index 2e98593..d1d8fac 100644 --- a/lib/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart +++ b/lib/screens/doctor_screen/doctor_dashboard/doctor_dashboard_home_screen.dart @@ -1,7 +1,10 @@ +import 'package:cloud_firestore/cloud_firestore.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: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 { const DoctorDashboardHomeScreen({super.key}); @@ -14,6 +17,7 @@ class DoctorDashboardHomeScreen extends StatefulWidget { class _DoctorDashboardHomeScreenState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; + Doctor? _doctorProfile; @override void initState() { @@ -23,12 +27,23 @@ class _DoctorDashboardHomeScreenState extends State duration: const Duration(milliseconds: 300), ); _animationController.forward(); + _fetchDoctorProfile(); } @override void dispose() { _animationController.dispose(); super.dispose(); + _fetchDoctorProfile(); + } + + Future _fetchDoctorProfile() async { + final doctorProfile = await DoctorProfileService.getDoctorProfile(); + if (mounted) { + setState(() { + _doctorProfile = doctorProfile; + }); + } } @override @@ -41,10 +56,17 @@ class _DoctorDashboardHomeScreenState extends State Expanded( child: ListView( 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 Widget _buildRealTimeCard() { return Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(15), // Reduced padding decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue[400]!, Colors.white], @@ -132,36 +154,286 @@ class _DoctorDashboardHomeScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Real-time care\nat your fingertips.', + 'Instant patient insights\nright at your fingertips', style: GoogleFonts.poppins( - fontSize: 30, + fontSize: 24, // Reduced font size fontWeight: FontWeight.bold, color: const Color.fromARGB(221, 67, 67, 67), ), ), - const SizedBox(height: 12), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SpecialtyScreen()), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.blue[700], - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + const SizedBox(height: 8), // Reduced spacing + // ElevatedButton( + // onPressed: () { + // // Navigator.push( + // // context, + // // MaterialPageRoute( + // // builder: (context) => const SpecialtyScreen()), + // // ); + // }, + // // style: ElevatedButton.styleFrom( + // // backgroundColor: Colors.white, + // // foregroundColor: Colors.blue[700], + // // padding: const EdgeInsets.symmetric( + // // horizontal: 12, vertical: 5), // Reduced padding + // // 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>>( + 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( - 'Start Consultation', - style: GoogleFonts.poppins( - fontWeight: FontWeight.bold, + const SizedBox(height: 12), + if (bookings.isNotEmpty) + SizedBox( + 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, + ), + ), + ], ), ), ], diff --git a/lib/screens/doctor_screen/doctor_dashboard/doctor_personal_profile_screen.dart b/lib/screens/doctor_screen/doctor_dashboard/doctor_personal_profile_screen.dart index da32eee..81f7f38 100644 --- a/lib/screens/doctor_screen/doctor_dashboard/doctor_personal_profile_screen.dart +++ b/lib/screens/doctor_screen/doctor_dashboard/doctor_personal_profile_screen.dart @@ -1,5 +1,7 @@ import 'package:firebase_auth/firebase_auth.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'; class DoctorPersonalProfileScreen extends StatefulWidget { @@ -13,6 +15,23 @@ class DoctorPersonalProfileScreen extends StatefulWidget { class _DoctorPersonalProfileScreen extends State { final FirebaseAuth _auth = FirebaseAuth.instance; // final GlobalKey _bottomNavigationKey = GlobalKey(); + Doctor? _doctorProfile; + + @override + void initState() { + super.initState(); + _fetchDoctorProfile(); + } + + Future _fetchDoctorProfile() async { + final doctorProfile = await DoctorProfileService.getDoctorProfile(); + if (mounted) { + setState(() { + _doctorProfile = doctorProfile; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -51,35 +70,45 @@ class _DoctorPersonalProfileScreen extends State { Container( width: 60, height: 60, - decoration: const BoxDecoration( + decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, + image: _doctorProfile?.profileImageUrl != null + ? DecorationImage( + image: NetworkImage(_doctorProfile!.profileImageUrl!), + fit: BoxFit.cover, + ) + : null, ), child: const Center( - child: Text( - 'D', - style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.blue, + // child: Text( + // _doctorProfile != null && _doctorProfile!.firstName != null + // ? _doctorProfile!.firstName![0].toUpperCase() + // : '', + // style: const TextStyle( + // fontSize: 30, + // fontWeight: FontWeight.bold, + // color: Colors.blue, + // ), + // ), ), - ), - ), ), const SizedBox(width: 16), - const Expanded( + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Doctor', - style: TextStyle( + _doctorProfile != null && _doctorProfile!.firstName != null + ? _doctorProfile!.firstName! + : 'Welcome', + style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), - Text( + const Text( 'Personal Profile', style: TextStyle( fontSize: 14, diff --git a/lib/screens/doctor_screen/doctor_dashboard/doctor_services_menu_screen.dart b/lib/screens/doctor_screen/doctor_dashboard/doctor_services_menu_screen.dart index af37272..f5ce836 100644 --- a/lib/screens/doctor_screen/doctor_dashboard/doctor_services_menu_screen.dart +++ b/lib/screens/doctor_screen/doctor_dashboard/doctor_services_menu_screen.dart @@ -1,5 +1,6 @@ -import 'package:firebase_auth/firebase_auth.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'; class DoctorServicesMenuScreen extends StatefulWidget { @@ -10,8 +11,23 @@ class DoctorServicesMenuScreen extends StatefulWidget { } class _DoctorServicesMenuScreen extends State { - final FirebaseAuth _auth = FirebaseAuth.instance; + Doctor? _doctorProfile; // final GlobalKey _bottomNavigationKey = GlobalKey(); + @override + void initState() { + super.initState(); + _fetchDoctorProfile(); + } + + Future _fetchDoctorProfile() async { + final doctorProfile = await DoctorProfileService.getDoctorProfile(); + if (mounted) { + setState(() { + _doctorProfile = doctorProfile; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -50,35 +66,48 @@ class _DoctorServicesMenuScreen extends State { Container( width: 60, height: 60, - decoration: const BoxDecoration( + decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, + image: _doctorProfile?.profileImageUrl != null + ? DecorationImage( + image: NetworkImage(_doctorProfile!.profileImageUrl!), + fit: BoxFit.cover, + ) + : null, ), - child: const Center( - child: Text( - 'D', - style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - ), + child: _doctorProfile?.profileImageUrl == null + ? Center( + child: Text( + _doctorProfile != null && + _doctorProfile!.firstName != null + ? _doctorProfile!.firstName![0].toUpperCase() + : '', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ) + : null, ), const SizedBox(width: 16), - const Expanded( + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Doctor', - style: TextStyle( + _doctorProfile != null && _doctorProfile!.firstName != null + ? _doctorProfile!.firstName! + : 'Welcome', + style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), - Text( + const Text( 'See our services below', style: TextStyle( fontSize: 14, @@ -88,11 +117,6 @@ class _DoctorServicesMenuScreen extends State { ], ), ), - const Icon( - Icons.chevron_right, - color: Colors.white, - size: 30, - ), ], ), ); @@ -115,7 +139,7 @@ class _DoctorServicesMenuScreen extends State { child: Column( children: [ _buildOptionTile( - 'Schedule consultation', + 'Consultation Centers', Icons.medical_information_outlined, onTap: () { Navigator.of(context) @@ -159,21 +183,4 @@ class _DoctorServicesMenuScreen extends State { onTap: onTap, ); } - - Future _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.')), - ); - } - } - } } diff --git a/lib/screens/doctor_screen/doctor_profile_screens/achivements_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/achivements_screen.dart index 455cccc..acdaa90 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/achivements_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/achivements_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/doctor_controller.dart'; - -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class AchievementsScreen extends StatefulWidget { final DoctorController controller; @@ -57,10 +56,6 @@ class _AchievementsScreenState extends State { _showError('Achievement must be at least 3 characters long'); return false; } - if (!RegExp(r'^[a-zA-Z0-9\s.,]+$').hasMatch(value)) { - _showError('Please enter valid achievement text'); - return false; - } return true; } diff --git a/lib/screens/doctor_screen/doctor_profile_screens/address_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/address_screen.dart index 2131156..d9f8a43 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/address_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/address_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/doctor_controller.dart'; - -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class DoctorAddressScreen extends StatefulWidget { final DoctorController? controller; @@ -32,121 +31,81 @@ class _DoctorAddressScreenState extends State { void initState() { super.initState(); _controller = widget.controller ?? DoctorController(); + _floorBuildingController = TextEditingController(); + _streetController = TextEditingController(); + _cityController = TextEditingController(); + _stateController = TextEditingController(); + _countryController = TextEditingController(); + _postalCodeController = TextEditingController(); + _addressTypeController = TextEditingController(); _loadSavedData(); } - @override - void dispose() { - _floorBuildingController.dispose(); - _streetController.dispose(); - _cityController.dispose(); - _stateController.dispose(); - _countryController.dispose(); - _postalCodeController.dispose(); - _addressTypeController.dispose(); - super.dispose(); - } - void _loadSavedData() { final doctor = _controller.model; - _floorBuildingController = - TextEditingController(text: doctor.floorBuilding ?? ''); - _streetController = TextEditingController(text: doctor.street ?? ''); - _cityController = TextEditingController(text: doctor.city ?? ''); - _stateController = TextEditingController(text: doctor.state ?? ''); - _countryController = TextEditingController(text: doctor.country ?? ''); - _postalCodeController = - TextEditingController(text: doctor.postalCode ?? ''); - _addressTypeController = - TextEditingController(text: doctor.addressType ?? ''); - selectedAddressType = widget.controller?.model.addressType; - if (selectedAddressType != null && - !addressTypes.contains(selectedAddressType)) { - showCustomTypeField = true; + _floorBuildingController.text = doctor.floorBuilding ?? ''; + _streetController.text = doctor.street ?? ''; + _cityController.text = doctor.city ?? ''; + _stateController.text = doctor.state ?? ''; + _countryController.text = doctor.country ?? ''; + _postalCodeController.text = doctor.postalCode ?? ''; + + // Proper handling of address type + if (doctor.addressType != null) { + final savedType = doctor.addressType!; + if (addressTypes.contains(savedType)) { + selectedAddressType = savedType; + showCustomTypeField = false; + } else { + selectedAddressType = 'Others'; + showCustomTypeField = true; + _addressTypeController.text = savedType; + } } } - // bool _validateAndProceed() {if (_formKey.currentState!.validate()) { - // // Update the address model - // _controller.updateFloorBuilding(_floorBuildingController.text); - // _controller.updateStreet(_streetController.text); - // _controller.updateCity(_cityController.text); - // _controller.updateState(_stateController.text); - // _controller.updateCountry(_countryController.text); - // _controller.updatePostalCode(_postalCodeController.text); + void _handleAddressTypeSelection(String type) { + setState(() { + if (type == 'Others') { + showCustomTypeField = true; + selectedAddressType = type; + // Don't clear the custom field if it was previously set + if (_addressTypeController.text.isEmpty) { + _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() { - // 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()) { + // Save all form data _controller.updateFloorBuilding(_floorBuildingController.text); _controller.updateStreet(_streetController.text); _controller.updateCity(_cityController.text); _controller.updateState(_stateController.text); _controller.updateCountry(_countryController.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 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() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -162,16 +121,13 @@ class _DoctorAddressScreenState extends State { const SizedBox(height: 8), LayoutBuilder( builder: (context, constraints) { - // Use the width of the layout to manage chip sizes final chipMaxWidth = constraints.maxWidth; return Wrap( - spacing: 6, // Adjusted spacing to help fit in a single row + spacing: 6, runSpacing: 6, children: addressTypes.map((type) { - final isSelected = - !showCustomTypeField && selectedAddressType == type || - (type == 'Others' && showCustomTypeField); + final isSelected = selectedAddressType == type; IconData icon; switch (type) { @@ -198,10 +154,10 @@ class _DoctorAddressScreenState extends State { maxWidth: (chipMaxWidth - (addressTypes.length - 1) * 6) / addressTypes.length, - ), // Calculate width to fit all chips within row + ), padding: const EdgeInsets.symmetric( - horizontal: 12, // Reduced horizontal padding - vertical: 8, // Reduced vertical padding + horizontal: 12, + vertical: 8, ), decoration: BoxDecoration( color: isSelected @@ -215,14 +171,12 @@ class _DoctorAddressScreenState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, - color: Colors.blue, size: 16), // Smaller icon - const SizedBox( - width: 4), // Spacing between icon and text + Icon(icon, color: Colors.blue, size: 16), + const SizedBox(width: 4), Text( type, style: TextStyle( - fontSize: 14, // Reduced font size + fontSize: 14, color: isSelected ? Colors.blue : Colors.black87, fontWeight: FontWeight.w500, ), @@ -239,33 +193,34 @@ class _DoctorAddressScreenState extends State { if (showCustomTypeField) ...[ const SizedBox(height: 16), TextFormField( - controller: _addressTypeController, - decoration: InputDecoration( - labelText: 'Custom Address Type', - prefixIcon: - const Icon(Icons.edit_location_alt, color: Colors.blue), - // filled: true, - // fillColor: Colors.grey[100], - contentPadding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 12), - enabledBorder: OutlineInputBorder( - 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), - ), + controller: _addressTypeController, + decoration: InputDecoration( + labelText: 'Custom Address Type', + prefixIcon: + const Icon(Icons.edit_location_alt, color: Colors.blue), + contentPadding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300, width: 1), ), - validator: (value) { - if (showCustomTypeField && (value == null || value.isEmpty)) { - return 'Please enter address type'; - } - return null; - }, - onChanged: (value) { + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.blue, width: 1.5), + ), + ), + validator: (value) { + if (showCustomTypeField && (value == null || value.isEmpty)) { + return 'Please enter address type'; + } + return null; + }, + onChanged: (value) { + if (value.isNotEmpty) { _controller.updateAddressType(value); - }), + } + }, + ), ], ], ); @@ -434,7 +389,7 @@ class _DoctorAddressScreenState extends State { if (isMandatory && (value == null || value.isEmpty)) { 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 null; diff --git a/lib/screens/doctor_screen/doctor_profile_screens/digital_signature_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/digital_signature_screen.dart index 85a0e66..51adf20 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/digital_signature_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/digital_signature_screen.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'dart:io'; import 'package:image_picker/image_picker.dart'; import 'package:medora/controllers/doctor_controller.dart'; -import '../../../data/services/doctor_profile_service.dart'; -import '../../../route/route_names.dart'; +import 'package:medora/data/services/doctor_profile_service.dart'; +import 'package:medora/route/route_names.dart'; class DigitalSignatureScreen extends StatefulWidget { final DoctorController controller; @@ -59,12 +59,14 @@ class _DigitalSignatureScreenState extends State { await DoctorProfileService.saveDoctorProfile(widget.controller); if (success) { + // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Doctor profile saved successfully!'), backgroundColor: Colors.green, ), ); + // ignore: use_build_context_synchronously Navigator.of(context).pushNamed(RouteNames.doctorLandingScreen); } else { _showError('Failed to save profile. Please try again.'); diff --git a/lib/screens/doctor_screen/doctor_profile_screens/doctor_profile_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/doctor_profile_screen.dart index d676952..8331e07 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/doctor_profile_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/doctor_profile_screen.dart @@ -23,7 +23,6 @@ class _ProfileUploadPageState extends State { String? _selectedTitle; final List _titles = ['Mr', 'Mrs', 'Miss']; - final Map _errors = {}; @override void initState() { @@ -44,8 +43,6 @@ class _ProfileUploadPageState extends State { super.dispose(); } - bool _isEditing = false; - void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -74,16 +71,10 @@ class _ProfileUploadPageState extends State { Future _getImage(ImageSource source) async { try { - final XFile? image = await _picker.pickImage( - source: source, - imageQuality: 80, - maxWidth: 1000, - maxHeight: 1000, - ); + final XFile? image = await _picker.pickImage(source: source); if (image != null) { setState(() { _image = File(image.path); - _isEditing = true; }); _controller.updateProfileImage(image.path); } @@ -156,15 +147,14 @@ class _ProfileUploadPageState extends State { ); } - void _updateCombinedName(String name) { - String fullName = '$_selectedTitle$name'; - _controller.updateSurName(fullName); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), actions: [ IconButton( onPressed: () { @@ -242,7 +232,6 @@ class _ProfileUploadPageState extends State { children: [ _buildUniformField( label: 'Name', - icon: Icons.person, child: Container(), // The child parameter is not used in this implementation diff --git a/lib/screens/doctor_screen/doctor_profile_screens/experience_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/experience_screen.dart index 927ad9c..3778544 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/experience_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/experience_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:medora/controllers/doctor_controller.dart'; - -import '../../../route/route_names.dart'; +import 'package:medora/data/services/doctor_profile_service.dart'; +import 'package:medora/route/route_names.dart'; class ExperienceScreen extends StatefulWidget { final DoctorController controller; @@ -21,7 +21,6 @@ class _ExperienceScreenState extends State { late final DoctorController _controller; late TextEditingController _selectedExperience; late TextEditingController _licenseController; - bool _isEditing = false; @override void initState() { @@ -44,9 +43,7 @@ class _ExperienceScreenState extends State { } void _onFieldChanged() { - setState(() { - _isEditing = true; - }); + setState(() {}); } void _showError(String message) { @@ -59,8 +56,17 @@ class _ExperienceScreenState extends State { ); } - bool _validateAndProceed() { + Future _validateAndProceed() async { 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.updateLicenseNumber(_licenseController.text.trim()); return true; @@ -74,8 +80,8 @@ class _ExperienceScreenState extends State { appBar: AppBar( actions: [ IconButton( - onPressed: () { - if (_validateAndProceed()) { + onPressed: () async { + if (await _validateAndProceed()) { Navigator.pushNamed( context, RouteNames.specialitiesScreeen, diff --git a/lib/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart index 7bc09b5..c021df0 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/profile_description_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/doctor_controller.dart'; -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class ProfileDescriptionScreen extends StatefulWidget { final DoctorController? controller; @@ -19,7 +19,6 @@ class _ProfileDescriptionScreenState extends State { final _formKey = GlobalKey(); late DoctorController? _controller; late TextEditingController _descriptionController; - bool _isEditing = false; final int _minDescriptionLength = 5; final int _maxDescriptionLength = 500; @@ -40,9 +39,7 @@ class _ProfileDescriptionScreenState extends State { } void _onDescriptionChanged() { - setState(() { - _isEditing = true; - }); + setState(() {}); } bool _validateDescription() { diff --git a/lib/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart index 6c847da..cdf2ae5 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/qualifications_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/doctor_controller.dart'; -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class QualificationsScreen extends StatefulWidget { final DoctorController? controller; @@ -18,11 +18,9 @@ class _QualificationsScreenState extends State { late DoctorController _controller; late TextEditingController _qualificationsController; late List qualifications; - bool _isEditing = false; bool _showOthersField = false; final _formKey = GlobalKey(); - // Predefined popular qualifications final List popularQualifications = [ 'MBBS', 'MD', @@ -42,6 +40,7 @@ class _QualificationsScreenState extends State { _controller = widget.controller ?? DoctorController(); _controller.model.qualifications ??= []; _qualificationsController = TextEditingController(); + // Create a deep copy of the qualifications list qualifications = List.from(_controller.model.qualifications ?? []); } @@ -50,6 +49,8 @@ class _QualificationsScreenState extends State { _showError('Please add at least one qualification'); return false; } + // Sync the qualifications with the controller before navigation + _controller.model.qualifications = List.from(qualifications); return true; } @@ -82,26 +83,10 @@ class _QualificationsScreenState extends State { if (_formKey.currentState!.validate() && _validateQualification(qualification)) { - // Check if qualification already exists (case-insensitive) - bool isDuplicate = qualifications - .any((q) => q.toLowerCase() == qualification.toLowerCase()); - - 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, - ), - ); - } + setState(() { + qualifications.add(qualification); + _qualificationsController.clear(); + }); } } @@ -114,23 +99,14 @@ class _QualificationsScreenState extends State { if (qualifications.contains(qualification)) { qualifications.remove(qualification); - _controller.removeQualification(qualification); } else { - // Check if qualification already exists (case-insensitive) bool isDuplicate = qualifications .any((q) => q.toLowerCase() == qualification.toLowerCase()); if (!isDuplicate) { qualifications.add(qualification); - _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, - ), - ); + _showError('This qualification has already been added'); } } }); @@ -138,10 +114,9 @@ class _QualificationsScreenState extends State { void _removeQualification(int index) { setState(() { + // Remove the qualification at the specified index qualifications.removeAt(index); - _isEditing = true; }); - _controller.removeQualification(_qualificationsController.text); } void _showError(String message) { @@ -156,175 +131,183 @@ class _QualificationsScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: [ - IconButton( - onPressed: () { - if (_validateBeforeNextPage()) { - Navigator.pushNamed( - context, - RouteNames.doctorAddressScreen, - arguments: _controller, - ); - } - }, - icon: const Icon(Icons.arrow_forward), - ), - ], - title: const Text('Qualifications'), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - 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), - ), - ], + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + // Sync qualifications with controller before popping + _controller.model.qualifications = List.from(qualifications); + return true; + }, + child: Scaffold( + appBar: AppBar( + actions: [ + IconButton( + onPressed: () { + if (_validateBeforeNextPage()) { + Navigator.pushNamed( + context, + RouteNames.doctorAddressScreen, + arguments: _controller, + ); + } + }, + icon: const Icon(Icons.arrow_forward), ), - child: Form( - key: _formKey, - child: Column( - 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'; + ], + title: const Text('Qualifications'), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + 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, + child: Column( + 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( - color: Colors.transparent, - child: InkWell( - onTap: () => _toggleQualification(qualification), - borderRadius: BorderRadius.circular(25), - child: Container( - decoration: BoxDecoration( - color: - isSelected || (isOthers && _showOthersField) - ? Colors.blue.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(25), - border: Border.all( + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _toggleQualification(qualification), + borderRadius: BorderRadius.circular(25), + child: Container( + decoration: BoxDecoration( color: isSelected || (isOthers && _showOthersField) - ? Colors.blue - : Colors.transparent, + ? Colors.blue.withOpacity(0.2) + : 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) - ? Colors.blue - : Colors.black87, + ), + ); + }, + ), + ), + 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), + ], + 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), + ), ), - ), - ); - }, - ), - ), - 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), ], - 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), - ], + ), ), ), ), diff --git a/lib/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart b/lib/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart index d52606c..a9df8f7 100644 --- a/lib/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart +++ b/lib/screens/doctor_screen/doctor_profile_screens/specialities_selection_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:medora/controllers/doctor_controller.dart'; -import '../../../route/route_names.dart'; +import 'package:medora/route/route_names.dart'; class SpecialitiesScreen extends StatefulWidget { final DoctorController controller; @@ -16,77 +16,223 @@ class SpecialitiesScreen extends StatefulWidget { class _SpecialitiesScreenState extends State { String? selectedSpeciality; late final DoctorController _controller; - late TextEditingController _speciality; - bool _isEditing = false; final List> specialities = [ { 'icon': Icons.child_care, 'label': 'Pediatric', - 'value': 'pediatric', - 'description': 'Specialist in child healthcare', + 'value': 'Pediatric', + 'description': 'Medical care for infants, children, and adolescents', }, { 'icon': Icons.medical_services, - 'label': 'Casual', - 'value': 'casual', - 'description': 'General healthcare provider', + 'label': 'General Medicine', + 'value': 'General Medicine', + 'description': + 'Primary healthcare for adults and general medical conditions', }, { - 'icon': Icons.coronavirus, - 'label': 'Corona', - 'value': 'corona', - 'description': 'COVID-19 specialist', + 'icon': Icons.family_restroom, + 'label': 'Family Medicine', + 'value': 'Family Medicine', + '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, 'label': 'Gynecology', - 'value': 'gynecology', - 'description': 'Women\'s health specialist', + 'value': 'Gynecology', + 'description': "Women's health and reproductive care", }, { - 'icon': Icons.medical_services_outlined, - 'label': 'Orthopedic', - 'value': 'orthopedic', - 'description': 'Bone and joint specialist', + 'icon': Icons.water_drop, + 'label': 'Urology', + 'value': 'Urology', + 'description': 'Urinary tract and male reproductive health', }, { - 'icon': Icons.remove_red_eye, - 'label': 'Eye', - 'value': 'eye', - 'description': 'Eye care specialist', + 'icon': Icons.biotech, + 'label': 'Endocrinology', + 'value': 'Endocrinology', + 'description': 'Hormone and metabolic disorders', }, { - 'icon': Icons.psychology, - 'label': 'Psychiatrist', - 'value': 'psychiatrist', - 'description': 'Mental health specialist', + 'icon': Icons.bloodtype, + 'label': 'Oncology', + 'value': 'Oncology', + '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', - 'value': 'dental', - 'description': 'Dental care specialist', + 'value': 'Dentistry', + 'description': 'Oral health and dental care', }, { - 'icon': Icons.person, - 'label': 'General Medicine', - 'value': 'general', - 'description': 'General practitioner', + 'icon': Icons.accessibility_new, + 'label': 'Physical Therapy', + 'value': 'Physical Therapy', + '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 void initState() { super.initState(); - _controller = widget.controller ?? DoctorController(); + _controller = widget.controller; _loadSavedData(); } - void _loadSavedData() { - final doctor = _controller.model; - _speciality = TextEditingController(text: doctor.speciality ?? ''); - } + void _loadSavedData() {} void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar( @@ -163,7 +309,6 @@ class _SpecialitiesScreenState extends State { setState(() { // _speciality = specialty['value']; selectedSpeciality = specialty['value']; - _isEditing = true; widget.controller.updateSpeciality(selectedSpeciality!); });