feature/medora-125 (#9)
fixed bugs Co-authored-by: DhanshCOSQ <dhanshas@cosq.net> Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com> Reviewed-on: cosqnet/telemednet#9 Reviewed-by: Benoy Bose <benoybose@cosq.net> Co-authored-by: Jipson George <jipsongeorge@cosq.net> Co-committed-by: Jipson George <jipsongeorge@cosq.net>
This commit is contained in:
parent
42543367a4
commit
520c9b6e44
@ -1,4 +1,4 @@
|
||||
import '../data/models/consultation_center.dart';
|
||||
import 'package:medora/data/models/consultation_center.dart';
|
||||
|
||||
class ConsultationCenterController {
|
||||
final ConsultationCenter model;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2,6 +2,7 @@ class Doctor {
|
||||
List<String>? 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<String, dynamic> toJson() => {
|
||||
'profileImagePath': profileImageUrl,
|
||||
'profileImage': profileImage,
|
||||
'achievements': achievements,
|
||||
'speciality': speciality,
|
||||
@ -67,6 +70,7 @@ class Doctor {
|
||||
|
||||
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
|
||||
achievements: List<String>.from(json['achievements'] ?? []),
|
||||
profileImageUrl: json['profileImageUrl'],
|
||||
profileImage: json['profileImage'],
|
||||
speciality: json['speciality'],
|
||||
yearsOfExperience: json['yearsOfExperience'],
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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<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 {
|
||||
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<bool> 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<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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<SignUpScreen> {
|
||||
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<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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -51,6 +170,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(
|
||||
@ -65,22 +185,25 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
),
|
||||
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<SignUpScreen> {
|
||||
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<SignUpScreen> {
|
||||
},
|
||||
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<SignUpScreen> {
|
||||
|
||||
Future<void> _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<SignUpScreen> {
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_emailFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<BusinessCenterScreen> {
|
||||
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<BusinessCenterScreen> {
|
||||
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<BusinessCenterScreen> {
|
||||
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<BusinessCenterScreen> {
|
||||
label: 'Postal Code',
|
||||
controller: _postalCodeController,
|
||||
onChanged: (value) =>
|
||||
widget.controller!.updatePostalCode(value),
|
||||
widget.controller.updatePostalCode(value),
|
||||
icon: Icons.mail,
|
||||
isMandatory: true,
|
||||
),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
// );
|
||||
},
|
||||
),
|
||||
@ -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),
|
||||
|
||||
@ -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<DoctorDashboardHomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
Doctor? _doctorProfile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -23,12 +27,23 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_animationController.forward();
|
||||
_fetchDoctorProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
_fetchDoctorProfile();
|
||||
}
|
||||
|
||||
Future<void> _fetchDoctorProfile() async {
|
||||
final doctorProfile = await DoctorProfileService.getDoctorProfile();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_doctorProfile = doctorProfile;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,10 +56,17 @@ class _DoctorDashboardHomeScreenState extends State<DoctorDashboardHomeScreen>
|
||||
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<DoctorDashboardHomeScreen>
|
||||
|
||||
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<DoctorDashboardHomeScreen>
|
||||
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<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(
|
||||
'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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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<DoctorPersonalProfileScreen> {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
// 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -51,35 +70,45 @@ class _DoctorPersonalProfileScreen extends State<DoctorPersonalProfileScreen> {
|
||||
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,
|
||||
|
||||
@ -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<DoctorServicesMenuScreen> {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
Doctor? _doctorProfile;
|
||||
// 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -50,35 +66,48 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
||||
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<DoctorServicesMenuScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -115,7 +139,7 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
_buildOptionTile(
|
||||
'Schedule consultation',
|
||||
'Consultation Centers',
|
||||
Icons.medical_information_outlined,
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
@ -159,21 +183,4 @@ class _DoctorServicesMenuScreen extends State<DoctorServicesMenuScreen> {
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
try {
|
||||
await _auth.signOut();
|
||||
if (mounted) {
|
||||
Navigator.of(context)
|
||||
.pushReplacementNamed(RouteNames.scheduleConsultationScreen);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error signing out: $e");
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to log out. Please try again.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package: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<AchievementsScreen> {
|
||||
_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;
|
||||
}
|
||||
|
||||
@ -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<DoctorAddressScreen> {
|
||||
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<DoctorAddressScreen> {
|
||||
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<DoctorAddressScreen> {
|
||||
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<DoctorAddressScreen> {
|
||||
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<DoctorAddressScreen> {
|
||||
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<DoctorAddressScreen> {
|
||||
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;
|
||||
|
||||
@ -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<DigitalSignatureScreen> {
|
||||
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.');
|
||||
|
||||
@ -23,7 +23,6 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
||||
String? _selectedTitle;
|
||||
|
||||
final List<String> _titles = ['Mr', 'Mrs', 'Miss'];
|
||||
final Map<String, String> _errors = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -44,8 +43,6 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isEditing = false;
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@ -74,16 +71,10 @@ class _ProfileUploadPageState extends State<ProfileUploadPage> {
|
||||
|
||||
Future<void> _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<ProfileUploadPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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<ProfileUploadPage> {
|
||||
children: [
|
||||
_buildUniformField(
|
||||
label: 'Name',
|
||||
|
||||
icon: Icons.person,
|
||||
child:
|
||||
Container(), // The child parameter is not used in this implementation
|
||||
|
||||
@ -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<ExperienceScreen> {
|
||||
late final DoctorController _controller;
|
||||
late TextEditingController _selectedExperience;
|
||||
late TextEditingController _licenseController;
|
||||
bool _isEditing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -44,9 +43,7 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
||||
}
|
||||
|
||||
void _onFieldChanged() {
|
||||
setState(() {
|
||||
_isEditing = true;
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
@ -59,8 +56,17 @@ class _ExperienceScreenState extends State<ExperienceScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
bool _validateAndProceed() {
|
||||
Future<bool> _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<ExperienceScreen> {
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (_validateAndProceed()) {
|
||||
onPressed: () async {
|
||||
if (await _validateAndProceed()) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
RouteNames.specialitiesScreeen,
|
||||
|
||||
@ -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<ProfileDescriptionScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<ProfileDescriptionScreen> {
|
||||
}
|
||||
|
||||
void _onDescriptionChanged() {
|
||||
setState(() {
|
||||
_isEditing = true;
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool _validateDescription() {
|
||||
|
||||
@ -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<QualificationsScreen> {
|
||||
late DoctorController _controller;
|
||||
late TextEditingController _qualificationsController;
|
||||
late List<String> qualifications;
|
||||
bool _isEditing = false;
|
||||
bool _showOthersField = false;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Predefined popular qualifications
|
||||
final List<String> popularQualifications = [
|
||||
'MBBS',
|
||||
'MD',
|
||||
@ -42,6 +40,7 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
||||
_controller = widget.controller ?? DoctorController();
|
||||
_controller.model.qualifications ??= [];
|
||||
_qualificationsController = TextEditingController();
|
||||
// Create a deep copy of the qualifications list
|
||||
qualifications = List<String>.from(_controller.model.qualifications ?? []);
|
||||
}
|
||||
|
||||
@ -50,6 +49,8 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
||||
_showError('Please add at least one qualification');
|
||||
return false;
|
||||
}
|
||||
// Sync the qualifications with the controller before navigation
|
||||
_controller.model.qualifications = List<String>.from(qualifications);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -82,26 +83,10 @@ class _QualificationsScreenState extends State<QualificationsScreen> {
|
||||
|
||||
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<QualificationsScreen> {
|
||||
|
||||
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<QualificationsScreen> {
|
||||
|
||||
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<QualificationsScreen> {
|
||||
|
||||
@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<String>.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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<SpecialitiesScreen> {
|
||||
String? selectedSpeciality;
|
||||
late final DoctorController _controller;
|
||||
late TextEditingController _speciality;
|
||||
bool _isEditing = false;
|
||||
|
||||
final List<Map<String, dynamic>> 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<SpecialitiesScreen> {
|
||||
setState(() {
|
||||
// _speciality = specialty['value'];
|
||||
selectedSpeciality = specialty['value'];
|
||||
_isEditing = true;
|
||||
widget.controller.updateSpeciality(selectedSpeciality!);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user