diff --git a/android/app/build.gradle b/android/app/build.gradle index e25bdfe..1c0f9e8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,7 +14,7 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { - namespace = "com.cosqnet.telemednet" + namespace = "com.cosqnet.medoraprovider" compileSdk = flutter.compileSdkVersion ndkVersion = "25.1.8937393" diff --git a/android/app/src/main/java/com/cosqnet/telemednet/MainActivity.java b/android/app/src/main/java/com/cosqnet/medoraprovider/MainActivity.java similarity index 75% rename from android/app/src/main/java/com/cosqnet/telemednet/MainActivity.java rename to android/app/src/main/java/com/cosqnet/medoraprovider/MainActivity.java index 16552a6..8195ccb 100644 --- a/android/app/src/main/java/com/cosqnet/telemednet/MainActivity.java +++ b/android/app/src/main/java/com/cosqnet/medoraprovider/MainActivity.java @@ -1,4 +1,4 @@ -package com.cosqnet.telemednet; +package com.cosqnet.medoraprovider; import io.flutter.embedding.android.FlutterActivity; diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index eb5bcea..752bf57 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -384,7 +384,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -401,7 +401,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -416,7 +416,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/controllers/patient_controller.dart b/lib/controllers/patient_controller.dart new file mode 100644 index 0000000..dc2b961 --- /dev/null +++ b/lib/controllers/patient_controller.dart @@ -0,0 +1,96 @@ +import 'package:medora/data/models/patient.dart'; +import '../data/services/patient_registration_service.dart'; + +class PatientController { + final PatientModel model = PatientModel(); + Map validationErrors = {}; + + void clearValidationErrors() { + validationErrors.clear(); + } + + void updateName(String name) { + model.name = name; + } + + void updatePhoneNumber(String phoneNumber) { + model.phoneNumber = phoneNumber; + } + + void updateGender(String gender) { + model.gender = gender; + } + + void updateDateOfBirth(DateTime dateOfBirth) { + model.dateOfBirth = dateOfBirth; + } + + void updateProfileImage(String imagePath) { + model.profileImagePath = imagePath; + } + + void updateHouseNo(String houseNo) { + model.address.houseNo = houseNo; + } + + void updateLine(String line) { + model.address.line = line; + } + + void updateTown(String town) { + model.address.town = town; + } + + void updatePincode(String pincode) { + model.address.pincode = pincode; + } + + void updateCountry(String country) { + model.address.country = country; + } + + void updateState(String state) { + model.address.state = state; + } + + void updateCity(String city) { + model.address.city = city; + } + + void addFamilyMember(FamilyMember member) { + model.familyMembers.add(member); + } + + void updateFamilyMember(int index, FamilyMember member) { + if (index >= 0 && index < model.familyMembers.length) { + model.familyMembers[index] = member; + } + } + + void deleteFamilyMember(int index) { + if (index >= 0 && index < model.familyMembers.length) { + model.familyMembers.removeAt(index); + } + } + + Future savePatientData() async { + return await PatientProfileService.savePatientProfile(this); + } + + Future loadPatientData() async { + PatientModel? loadedModel = await PatientProfileService.getPatientProfile(); + if (loadedModel != null) { + model.updateFrom(loadedModel); + return true; + } + return false; + } + + Future updatePatientData() async { + return await PatientProfileService.updatePatientProfile(model); + } + + Future deletePatientData() async { + return await PatientProfileService.deletePatientProfile(); + } +} diff --git a/lib/data/models/patient.dart b/lib/data/models/patient.dart new file mode 100644 index 0000000..52fc057 --- /dev/null +++ b/lib/data/models/patient.dart @@ -0,0 +1,126 @@ +class PatientModel { + String? name; + String? phoneNumber; + String? gender; + DateTime? dateOfBirth; + String? profileImagePath; + String? profileImageUrl; + PatientAddress address; + + List familyMembers = []; + + PatientModel() : address = PatientAddress(); + + Map toJson() { + return { + 'name': name, + 'phoneNumber': phoneNumber, + 'gender': gender, + 'dateOfBirth': dateOfBirth?.toIso8601String(), + 'profileImagePath': profileImagePath, + 'profileImageUrl': profileImageUrl, + 'address': address.toJson(), + 'familyMembers': familyMembers.map((member) => member.toJson()).toList(), + }; + } + + PatientModel.fromJson(Map json) + : address = PatientAddress() { + name = json['name']; + phoneNumber = json['phoneNumber']; + gender = json['gender']; + dateOfBirth = json['dateOfBirth'] != null + ? DateTime.parse(json['dateOfBirth']) + : null; + profileImagePath = json['profileImagePath']; + profileImageUrl = json['profileImageUrl']; + if (json['address'] != null) { + address = + PatientAddress.fromJson(json['address'] as Map); + } + if (json['familyMembers'] != null) { + familyMembers = (json['familyMembers'] as List) + .map((memberJson) => FamilyMember.fromJson(memberJson)) + .toList(); + } + } + + void updateFrom(PatientModel other) { + name = other.name; + phoneNumber = other.phoneNumber; + gender = other.gender; + dateOfBirth = other.dateOfBirth; + profileImagePath = other.profileImagePath; + profileImageUrl = other.profileImageUrl; + address = other.address; + familyMembers = other.familyMembers; + } +} + +class FamilyMember { + String? name; + String? relation; + String? gender; + DateTime? dateOfBirth; + + FamilyMember({this.name, this.relation, this.gender, this.dateOfBirth}); + + Map toJson() { + return { + 'name': name, + 'relation': relation, + 'gender': gender, + 'dateOfBirth': dateOfBirth?.toIso8601String(), + }; + } + + FamilyMember.fromJson(Map json) { + name = json['name']; + relation = json['relation']; + gender = json['gender']; + dateOfBirth = json['dateOfBirth'] != null + ? DateTime.parse(json['dateOfBirth']) + : null; + } +} + +class PatientAddress { + String? houseNo; + String? line; + String? town; + String? pincode; + String? country; + String? state; + String? city; + + PatientAddress( + {this.houseNo, + this.line, + this.town, + this.pincode, + this.country, + this.state, + this.city}); + + Map toJson() { + return { + 'houseNo': houseNo, + 'line': line, + 'town': town, + 'pincode': pincode, + 'country': country, + 'state': state, + 'city': city, + }; + } + + PatientAddress.fromJson(Map json) { + houseNo = json['houseNo']; + line = json['line']; + town = json['town']; + pincode = json['pincode']; + country = json['country']; + state = json['state']; + city = json['city']; + } +} diff --git a/lib/data/services/patient_registration_service.dart b/lib/data/services/patient_registration_service.dart new file mode 100644 index 0000000..fd91ded --- /dev/null +++ b/lib/data/services/patient_registration_service.dart @@ -0,0 +1,198 @@ +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:medora/controllers/patient_controller.dart'; +import 'package:medora/data/models/patient.dart'; +import 'package:path/path.dart' as path; + +class PatientProfileService { + static final String patientProfileCollectionName = + dotenv.env['PATIENT_PROFILE_COLLECTION_NAME']!; + static final FirebaseFirestore db = FirebaseFirestore.instance; + static final FirebaseStorage storage = FirebaseStorage.instanceFor( + bucket: dotenv.env['FIREBASE_STORAGE_BUCKET']!); + + static Future uploadProfileImage(File imageFile) async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return null; + } + final String uid = user.uid; + final String fileName = + 'profile_${uid}_${DateTime.now().millisecondsSinceEpoch}${path.extension(imageFile.path)}'; + final Reference storageRef = + storage.ref().child('profile_images/$fileName'); + final UploadTask uploadTask = storageRef.putFile( + imageFile, + SettableMetadata( + contentType: 'image/${path.extension(imageFile.path).substring(1)}', + customMetadata: { + 'userId': uid, + 'uploadedAt': DateTime.now().toIso8601String(), + }, + ), + ); + final TaskSnapshot snapshot = await uploadTask; + final String downloadUrl = await snapshot.ref.getDownloadURL(); + + print('Profile image uploaded successfully'); + return downloadUrl; + } catch (e) { + print('Error uploading profile image: $e'); + return null; + } + } + + static Future deleteProfileImage(String imageUrl) async { + try { + final Reference storageRef = storage.refFromURL(imageUrl); + await storageRef.delete(); + print('Profile image deleted successfully'); + return true; + } catch (e) { + print('Error deleting profile image: $e'); + return false; + } + } + + static Future savePatientProfile(PatientController controller) async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return false; + } + + final String uid = user.uid; + final PatientModel patientData = controller.model; + String? imageUrl; + if (patientData.profileImagePath != null) { + final File imageFile = File(patientData.profileImagePath!); + imageUrl = await uploadProfileImage(imageFile); + if (imageUrl == null) { + print('Failed to upload profile image'); + return false; + } + } + + final Map patientJson = patientData.toJson(); + patientJson['createdAt'] = FieldValue.serverTimestamp(); + patientJson['updatedAt'] = FieldValue.serverTimestamp(); + patientJson['uid'] = uid; + patientJson['profileImageUrl'] = imageUrl; + + await db + .collection(patientProfileCollectionName) + .doc(uid) + .set(patientJson); + + print('Patient profile saved successfully'); + return true; + } catch (e) { + print('Error saving patient profile: $e'); + return false; + } + } + + static Future updatePatientProfile(PatientModel patient) async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return false; + } + + final String uid = user.uid; + String? imageUrl; + if (patient.profileImagePath != null) { + final DocumentSnapshot oldDoc = + await db.collection(patientProfileCollectionName).doc(uid).get(); + if (oldDoc.exists) { + final oldData = oldDoc.data() as Map; + final String? oldImageUrl = oldData['profileImageUrl']; + if (oldImageUrl != null) { + await deleteProfileImage(oldImageUrl); + } + } + final File imageFile = File(patient.profileImagePath!); + imageUrl = await uploadProfileImage(imageFile); + if (imageUrl == null) { + print('Failed to upload new profile image'); + return false; + } + } + + final Map patientJson = patient.toJson(); + patientJson['updatedAt'] = FieldValue.serverTimestamp(); + if (imageUrl != null) { + patientJson['profileImageUrl'] = imageUrl; + } + + await db + .collection(patientProfileCollectionName) + .doc(uid) + .update(patientJson); + + print('Patient profile updated successfully'); + return true; + } catch (e) { + print('Error updating patient profile: $e'); + return false; + } + } + + static Future deletePatientProfile() async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return false; + } + + final String uid = user.uid; + final DocumentSnapshot doc = + await db.collection(patientProfileCollectionName).doc(uid).get(); + if (doc.exists) { + final data = doc.data() as Map; + final String? imageUrl = data['profileImageUrl']; + if (imageUrl != null) { + await deleteProfileImage(imageUrl); + } + } + + await db.collection(patientProfileCollectionName).doc(uid).delete(); + + print('Patient profile deleted successfully'); + return true; + } catch (e) { + print('Error deleting patient profile: $e'); + return false; + } + } + + static Future getPatientProfile() async { + try { + final User? user = FirebaseAuth.instance.currentUser; + if (user == null) { + print('No user logged in'); + return null; + } + final String uid = user.uid; + final DocumentSnapshot doc = + await db.collection(patientProfileCollectionName).doc(uid).get(); + if (!doc.exists) { + print('No patient profile found for this user'); + return null; + } + final data = doc.data() as Map; + return PatientModel.fromJson(data); + } catch (e) { + print('Error fetching patient profile: $e'); + return null; + } + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index a27c338..c7ed946 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -85,5 +85,4 @@ class DefaultFirebaseOptions { storageBucket: 'cosq-telemednet-dev.appspot.com', measurementId: 'G-BBV9TFGNN5', ); - -} \ No newline at end of file +} diff --git a/lib/screens/patient_screens/appoinment_bookings/consultation_booking_screen.dart b/lib/screens/patient_screens/appoinment_bookings/consultation_booking_screen.dart new file mode 100644 index 0000000..f1e9102 --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/consultation_booking_screen.dart @@ -0,0 +1,842 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; +import 'package:medora/data/models/consultation_center.dart'; +import 'package:medora/data/models/doctor.dart'; +import 'package:medora/data/models/patient.dart'; +import 'package:medora/data/services/consultation_booking_service.dart'; +import 'package:medora/data/services/patient_registration_service.dart'; +import 'package:medora/route/route_names.dart'; +import 'package:medora/widgets/alert_screen.dart'; + +class ConsultationBookingScreen extends StatefulWidget { + final Doctor doctor; + final ConsultationCenter selectedConsultation; + final DateTime selectedDate; + final String selectedTime; + + const ConsultationBookingScreen({ + super.key, + required this.doctor, + required this.selectedConsultation, + required this.selectedDate, + required this.selectedTime, + }); + + @override + State createState() => + _ConsultationBookingScreenState(); +} + +class _ConsultationBookingScreenState extends State { + PatientModel? selectedPatient; + List familyMembers = []; + FamilyMember? selectedFamilyMember; + bool isLoading = true; + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _relationController = TextEditingController(); + DateTime? _selectedDateOfBirth; + String _selectedGender = 'Male'; + + @override + void dispose() { + _nameController.dispose(); + _relationController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _loadPatientProfile(); + } + + Future _loadPatientProfile() async { + setState(() => isLoading = true); + try { + final currentPatient = await PatientProfileService.getPatientProfile(); + if (currentPatient != null) { + setState(() { + selectedPatient = currentPatient; + }); + } + } catch (e) { + print('Error loading patient data: $e'); + } finally { + setState(() => isLoading = false); + } + } + + String get formattedAddress { + final parts = [ + widget.selectedConsultation.floorBuilding, + widget.selectedConsultation.street, + widget.selectedConsultation.city, + widget.selectedConsultation.state, + widget.selectedConsultation.postalCode + ].where((part) => part != null && part.isNotEmpty).toList(); + return parts.join(', '); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + appBar: _buildAppBar(context), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAppointmentCard(), + const SizedBox(height: 24), + _buildDoctorDetails(), + const SizedBox(height: 24), + _buildLocationDetails(), + const SizedBox(height: 24), + _buildPaymentDetails(), + const SizedBox(height: 24), + _buildInClinicAppointmentText(), + const SizedBox(height: 24), + _buildConfirmButton(context), + ], + ), + ), + ), + ); + } + + Widget _buildInClinicAppointmentText() { + String patientName = + selectedFamilyMember?.name ?? selectedPatient?.name ?? 'Select Patient'; + String relation = selectedFamilyMember?.relation != null + ? ' (${selectedFamilyMember!.relation})' + : ''; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: RichText( + text: TextSpan( + style: GoogleFonts.poppins( + fontSize: 16, + color: Colors.black87, + ), + children: [ + const TextSpan(text: 'In-clinic appointment for '), + TextSpan( + text: '$patientName$relation', + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + TextButton( + onPressed: _showPatientSelectionDialog, + child: Text( + 'Change', + style: GoogleFonts.poppins( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } + + PreferredSizeWidget _buildAppBar(BuildContext context) { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + title: Text( + 'Booking Overview', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ); + } + + Widget _buildAppointmentCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.calendar_today, color: Colors.white), + const SizedBox(width: 12), + Text( + DateFormat('EEEE, MMMM d').format(widget.selectedDate), + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.access_time, color: Colors.white), + const SizedBox(width: 12), + Text( + widget.selectedTime, + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDoctorDetails() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + widget.doctor.profileImageUrl!, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 80, + color: Colors.grey[300], + child: Icon(Icons.person, size: 40, color: Colors.grey[600]), + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.doctor.firstName ?? '', + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.doctor.speciality!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + Text( + '${widget.doctor.yearsOfExperience} years experience', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLocationDetails() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Location', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + formattedAddress, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes} minutes', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + Widget _buildPaymentDetails() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Payment Details', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Consultation Fee', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + Text( + '₹${widget.selectedConsultation.consultationFee ?? "500"}', + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildConfirmButton(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // Handle payment and booking confirmation + _showConfirmationDialog(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Confirm & Pay', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ); + } + + void _showConfirmationDialog(BuildContext context) async { + if (selectedPatient == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please select a patient for the appointment'), + backgroundColor: Colors.red, + ), + ); + return; + } + + final bookingService = BookingService(); + final currentUser = FirebaseAuth.instance.currentUser; + + // Get the correct patient name based on selection + final patientName = selectedFamilyMember != null + ? selectedFamilyMember!.name + : selectedPatient!.name; + + try { + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + } + + final bookingId = await bookingService.createBooking( + doctorId: widget.doctor.uid!, + profileImageUrl: widget.doctor.profileImageUrl!, + doctorName: widget.doctor.firstName ?? 'Doctor', + patientId: currentUser!.uid, + patientName: patientName ?? 'Patient', + location: formattedAddress, + appointmentDate: widget.selectedDate, + appointmentTime: widget.selectedTime, + consultationFee: + int.parse(widget.selectedConsultation.consultationFee ?? "500"), + specialization: widget.doctor.speciality!, + ); + + if (context.mounted) { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AlertScreen( + arguments: AlertArguments( + title: 'Booking Confirmed', + message: + 'Your in-clinic appointment has been successfully booked for $patientName. Booking ID: ${bookingId.substring(0, 8)}\n\nPlease complete the payment to confirm your appointment.', + actionTitle: 'View Appointments', + type: AlertType.success, + onActionPressed: () { + Navigator.pushReplacementNamed( + context, RouteNames.patientDashboardScreen); + }, + ), + ), + ), + ); + } + } catch (e) { + if (context.mounted) { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AlertScreen( + arguments: AlertArguments( + title: 'Booking Failed', + message: 'Unable to create booking. ${e.toString()}', + actionTitle: 'Try Again', + type: AlertType.error, + onActionPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ), + ); + } + } + } + + Future _showAddFamilyMemberDialog() async { + _nameController.clear(); + _relationController.clear(); + setState(() { + _selectedDateOfBirth = null; + _selectedGender = 'Male'; + }); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) => StatefulBuilder( + builder: (BuildContext context, StateSetter setDialogState) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + 'Add Family Member', + style: GoogleFonts.poppins(fontWeight: FontWeight.w600), + ), + content: AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'Full Name', + labelStyle: GoogleFonts.poppins(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon( + Icons.person_outline, + color: Colors.blue, + ), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _relationController, + decoration: InputDecoration( + labelText: 'Relation', + labelStyle: GoogleFonts.poppins(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon( + Icons.family_restroom, + color: Colors.blue, + ), + ), + ), + const SizedBox(height: 16), + InkWell( + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.blue, + onPrimary: Colors.white, + surface: Colors.grey[100]!, + ), + ), + child: child!, + ); + }, + ); + if (picked != null) { + setDialogState(() { + _selectedDateOfBirth = picked; + }); + } + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.calendar_today, + color: Colors.blue), + const SizedBox(width: 12), + Text( + _selectedDateOfBirth != null + ? DateFormat('dd/MM/yyyy') + .format(_selectedDateOfBirth!) + : 'Select Date of Birth', + style: GoogleFonts.poppins(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonHideUnderline( + child: DropdownButtonFormField( + value: _selectedGender, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.person_outline, + color: Colors.blue), + border: InputBorder.none, + labelStyle: GoogleFonts.poppins(), + ), + items: ['Male', 'Female', 'Other'] + .map((gender) => DropdownMenuItem( + value: gender, + child: Text(gender, + style: GoogleFonts.poppins()), + )) + .toList(), + onChanged: (value) { + if (value != null) { + setDialogState(() => _selectedGender = value); + } + }, + ), + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: GoogleFonts.poppins(color: Colors.grey), + ), + ), + ElevatedButton( + onPressed: () => _addFamilyMember(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Add Member', + style: GoogleFonts.poppins(color: Colors.white), + ), + ), + ], + ); + }, + ), + ); + } + + Future _addFamilyMember(BuildContext context) async { + if (_nameController.text.isEmpty || + _relationController.text.isEmpty || + _selectedDateOfBirth == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Please fill in all fields', + style: GoogleFonts.poppins(), + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + return; + } + + try { + final newFamilyMember = FamilyMember( + name: _nameController.text, + relation: _relationController.text, + gender: _selectedGender, + dateOfBirth: _selectedDateOfBirth, + ); + + if (selectedPatient != null) { + selectedPatient!.familyMembers.add(newFamilyMember); + await PatientProfileService.updatePatientProfile(selectedPatient!); + + setState(() { + selectedFamilyMember = newFamilyMember; + }); + } + + if (context.mounted) { + Navigator.pop(context); + + _showPatientSelectionDialog(); + } + } catch (e) { + if (context.mounted) { + Navigator.pop(context); // Pop add family member dialog + } + } + } + + void _showPatientSelectionDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + 'Select Patient', + style: GoogleFonts.poppins(fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: double.maxFinite, + child: ListView( + shrinkWrap: true, + children: [ + // Main patient + _buildPatientTile( + name: selectedPatient?.name ?? '', + subtitle: 'Primary Patient', + isSelected: selectedFamilyMember == null, + onTap: () { + setState(() { + selectedFamilyMember = null; + }); + Navigator.pop(context); + }, + ), + const Divider(), + // Family members + ...selectedPatient?.familyMembers.map( + (member) => _buildPatientTile( + name: member.name ?? '', + subtitle: member.relation ?? '', + isSelected: selectedFamilyMember == member, + onTap: () { + setState(() { + selectedFamilyMember = member; + }); + Navigator.pop(context); + }, + ), + ) ?? + [], + const Divider(), + ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.person_add, color: Colors.blue), + ), + title: Text( + 'Add Family Member', + style: GoogleFonts.poppins( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + _showAddFamilyMemberDialog(); + }, + ), + ], + ), + ), + ), + ); + } + + Widget _buildPatientTile({ + required String name, + required String subtitle, + required bool isSelected, + required VoidCallback onTap, + }) { + return ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.withOpacity(0.1) + : Colors.grey.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.person, + color: isSelected ? Colors.blue : Colors.grey, + ), + ), + title: Text( + name, + style: GoogleFonts.poppins( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected ? Colors.blue : Colors.black87, + ), + ), + subtitle: Text( + subtitle, + style: GoogleFonts.poppins( + color: Colors.grey[600], + ), + ), + trailing: isSelected + ? const Icon(Icons.check_circle, color: Colors.blue) + : null, + onTap: onTap, + ); + } +} diff --git a/lib/screens/patient_screens/appoinment_bookings/consultation_time_screen.dart b/lib/screens/patient_screens/appoinment_bookings/consultation_time_screen.dart new file mode 100644 index 0000000..7ee76c7 --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/consultation_time_screen.dart @@ -0,0 +1,553 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:medora/data/models/consultation_center.dart'; +import 'package:medora/data/models/doctor.dart'; +import 'package:intl/intl.dart'; +import 'package:medora/route/route_names.dart'; + +class ConsultationTimeScreen extends StatefulWidget { + final Doctor doctor; + final ConsultationCenter selectedConsultation; + + const ConsultationTimeScreen({ + super.key, + required this.doctor, + required this.selectedConsultation, + }); + + @override + State createState() => _ConsultationTimeScreenState(); +} + +class _ConsultationTimeScreenState extends State { + DateTime? selectedDate; + String? selectedTime; + + List getTimeSlotsForDay(String dayName) { + try { + final schedule = widget.selectedConsultation.weeklySchedule?.firstWhere( + (schedule) => schedule.day == dayName, + orElse: () => AvailabilitySchedule( + day: dayName, + timeSlots: [], + ), + ); + return schedule?.timeSlots ?? []; + } catch (e) { + debugPrint('Error getting time slots: $e'); + return []; + } + } + + DateTime? parseTimeString(String? timeStr) { + if (timeStr == null) return null; + + try { + // Try parsing 12-hour format first + return DateFormat('h:mm a').parse(timeStr); + } catch (e) { + try { + // Try parsing 24-hour format + return DateFormat('HH:mm').parse(timeStr); + } catch (e) { + debugPrint('Error parsing time: $timeStr'); + return null; + } + } + } + + String get formattedAddress { + final parts = [ + widget.selectedConsultation.floorBuilding, + widget.selectedConsultation.street, + widget.selectedConsultation.city, + widget.selectedConsultation.state, + widget.selectedConsultation.postalCode + ].where((part) => part != null && part.isNotEmpty).toList(); + return parts.join(', '); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + appBar: _buildAppBar(), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLocationInfo(), + const SizedBox(height: 24), + _buildDateSelection(), + const SizedBox(height: 24), + _buildTimeSlots(), + ], + ), + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + title: Text( + 'Select Date & Time', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ); + } + + Widget _buildLocationInfo() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.doctor.firstName ?? '', + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.doctor.speciality!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + widget.doctor.profileImageUrl!, + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 60, + height: 60, + color: Colors.grey[300], + child: + Icon(Icons.person, size: 30, color: Colors.grey[600]), + ); + }, + ), + ), + ], + ), + const Divider(height: 24), + Text( + 'Selected Location', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + widget.selectedConsultation.city ?? '', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + 'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes}', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + Widget _buildDateSelection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Select Date', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 20, // Show next 20 days + itemBuilder: (context, index) { + final date = DateTime.now().add(Duration(days: index)); + final isSelected = selectedDate?.day == date.day && + selectedDate?.month == date.month && + selectedDate?.year == date.year; + final isAvailable = _isDateAvailable(date); + + return GestureDetector( + onTap: isAvailable + ? () { + setState(() { + selectedDate = date; + selectedTime = null; // Reset time when date changes + }); + } + : null, + child: Container( + width: 70, + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue + : isAvailable + ? Colors.white + : Colors.grey[200], + borderRadius: BorderRadius.circular(16), + boxShadow: isAvailable + ? [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.white + : isAvailable + ? Colors.grey[600] + : Colors.grey[400], + ), + ), + const SizedBox(height: 8), + Text( + date.day.toString(), + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : isAvailable + ? Colors.black87 + : Colors.grey[400], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildTimeSlots() { + if (selectedDate == null) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.5), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withOpacity(0.2)), + ), + child: Center( + child: Text( + 'Please select a date to view available time slots', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ); + } + + final dayName = DateFormat('EEEE').format(selectedDate!); + final timeSlots = getTimeSlotsForDay(dayName); + final allTimeSlots = _generateTimeSlots(timeSlots); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Select Time', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Wrap( + spacing: 12, + runSpacing: 12, + children: allTimeSlots.map((time) { + final isSelected = selectedTime == time; + final isAvailable = _isTimeSlotAvailable(time); + + return GestureDetector( + onTap: isAvailable + ? () { + Navigator.pushNamed( + context, + RouteNames.consultationBookingScreen, + arguments: { + 'doctor': widget.doctor, + 'selectedConsultation': widget.selectedConsultation, + 'selectedDate': selectedDate, + 'selectedTime': time + }, + ); + } + : null, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue + : isAvailable + ? Colors.white + : Colors.grey[200], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? Colors.blue + : isAvailable + ? Colors.grey.withOpacity(0.2) + : Colors.grey.withOpacity(0.1), + ), + ), + child: Text( + time, + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.white + : isAvailable + ? Colors.black87 + : Colors.grey[400], + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ); + } + + bool _isDateAvailable(DateTime date) { + final dayName = DateFormat('EEEE').format(date); + return widget.selectedConsultation.weeklySchedule + ?.any((schedule) => schedule.day == dayName) ?? + false; + } + + List _generateTimeSlots(List timeSlots) { + final slots = []; + final timeFormat = DateFormat('h:mm a'); + + for (var slot in timeSlots) { + final startTime = parseTimeString(slot.startTime); + final endTime = parseTimeString(slot.endTime); + + if (startTime == null || endTime == null) continue; + + var currentTime = startTime; + while (currentTime.isBefore(endTime)) { + slots.add(timeFormat.format(currentTime)); + currentTime = currentTime.add(const Duration(minutes: 30)); + } + } + + return slots; + } + + bool _isTimeSlotAvailable(String time) { + final now = DateTime.now(); + + if (selectedDate == null) return false; + + // Parse the time slot + final timeSlot = parseTimeString(time); + if (timeSlot == null) return false; + + // Create a DateTime combining selected date and time + final slotDateTime = DateTime( + selectedDate!.year, + selectedDate!.month, + selectedDate!.day, + timeSlot.hour, + timeSlot.minute, + ); + + // Check if the slot is in the past + if (slotDateTime.isBefore(now)) return false; + + // Here you would typically check against your booking database + // For now, returning true for future slots + return true; + } + + void _handleBooking() { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Text( + 'Confirm Booking', + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildConfirmationDetail( + 'Doctor', + widget.doctor.firstName!, + ), + const SizedBox(height: 8), + _buildConfirmationDetail( + 'Location', + widget.selectedConsultation.city!, + ), + const SizedBox(height: 8), + _buildConfirmationDetail( + 'Date', + DateFormat('EEEE, MMMM d').format(selectedDate!), + ), + const SizedBox(height: 8), + _buildConfirmationDetail( + 'Time', + selectedTime!, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: GoogleFonts.poppins( + color: Colors.grey[600], + ), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Confirm', + style: GoogleFonts.poppins( + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget _buildConfirmationDetail(String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text( + '$label:', + style: GoogleFonts.poppins( + fontWeight: FontWeight.w500, + color: Colors.grey[600], + ), + ), + ), + Expanded( + child: Text( + value, + style: GoogleFonts.poppins( + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/patient_screens/appoinment_bookings/consultations_center_screen.dart b/lib/screens/patient_screens/appoinment_bookings/consultations_center_screen.dart new file mode 100644 index 0000000..d03f98b --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/consultations_center_screen.dart @@ -0,0 +1,312 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:medora/data/models/doctor.dart'; +import 'package:medora/data/models/consultation_center.dart'; +import 'package:medora/data/services/consultation_center_service.dart'; +import 'package:medora/route/route_names.dart'; + +class ConsultationsCenterScreen extends StatefulWidget { + final Doctor doctor; + + const ConsultationsCenterScreen({ + super.key, + required this.doctor, + }); + + @override + State createState() => + _ConsultationsCenterScreenState(); +} + +class _ConsultationsCenterScreenState extends State { + List _consultationCenters = []; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchDoctorConsultationCenters(); + } + + Future _fetchDoctorConsultationCenters() async { + try { + setState(() { + _isLoading = true; + _error = null; + }); + + if (widget.doctor.uid == null) { + throw Exception('Doctor UID is missing'); + } + + final centers = + await ConsultationCenterService.getDoctorConsultationCenters( + widget.doctor.uid!, + ); + + if (mounted) { + setState(() { + _consultationCenters = centers; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to load consultation centers: $e')), + ); + } + } + } + + String _formatAddress(ConsultationCenter center) { + List addressParts = []; + + if (center.floorBuilding != null && center.floorBuilding!.isNotEmpty) { + addressParts.add(center.floorBuilding!); + } + if (center.street != null && center.street!.isNotEmpty) { + addressParts.add(center.street!); + } + if (center.city != null && center.city!.isNotEmpty) { + addressParts.add(center.city!); + } + + return addressParts.join(', '); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + appBar: _buildAppBar(), + body: RefreshIndicator( + onRefresh: _fetchDoctorConsultationCenters, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDoctorInfo(), + const SizedBox(height: 24), + _buildConsultationLocations(), + ], + ), + ), + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + title: Text( + 'Select Location', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ); + } + + Widget _buildDoctorInfo() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + widget.doctor.profileImageUrl!, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 80, + color: Colors.grey[300], + child: Icon(Icons.person, size: 40, color: Colors.grey[600]), + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.doctor.firstName ?? "", + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.doctor.speciality!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + Text( + '${widget.doctor.yearsOfExperience} years experience', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildConsultationLocations() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Select Location', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Center( + child: Column( + children: [ + Text( + 'Error loading centers', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.red, + ), + ), + TextButton( + onPressed: _fetchDoctorConsultationCenters, + child: const Text('Retry'), + ), + ], + ), + ) + else if (_consultationCenters.isEmpty) + Center( + child: Text( + 'No consultation centers available', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ) + else + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _consultationCenters.length, + itemBuilder: (context, index) { + final center = _consultationCenters[index]; + + return GestureDetector( + onTap: () { + Navigator.pushNamed( + context, + RouteNames.consultationTimeScreen, + arguments: { + 'doctor': widget.doctor, + 'selectedConsultation': center, + }, + ); + }, + child: Container( + width: 200, + margin: const EdgeInsets.only(right: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _formatAddress(center), + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + if (center.averageDurationMinutes != null) + Text( + 'Average time: ${center.averageDurationMinutes} mins', + style: GoogleFonts.poppins( + fontSize: 12, + color: Colors.grey[600], + ), + ), + if (center.consultationFee != null) + Text( + 'Fee: ${center.consultationFee}', + style: GoogleFonts.poppins( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/screens/patient_screens/appoinment_bookings/doctor_details_screen.dart b/lib/screens/patient_screens/appoinment_bookings/doctor_details_screen.dart new file mode 100644 index 0000000..02e1b87 --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/doctor_details_screen.dart @@ -0,0 +1,346 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:medora/data/models/doctor.dart'; +import 'package:medora/route/route_names.dart'; +import 'package:shimmer/shimmer.dart'; + +class DoctorDetailsScreen extends StatefulWidget { + final Doctor doctor; + final ImageProvider? preloadedImage; + + const DoctorDetailsScreen({ + super.key, + required this.doctor, + this.preloadedImage, + }); + + @override + State createState() => _DoctorDetailsScreenState(); +} + +class _DoctorDetailsScreenState extends State { + bool isDescriptionExpanded = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + body: Column( + children: [ + _buildAppBar(context), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDoctorCard(), + const SizedBox(height: 24), + _buildDescription(), + const SizedBox(height: 24), + _buildQualifications(), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pushNamed( + context, RouteNames.consultationCenterScreen, + arguments: { + 'doctor': widget.doctor, + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Text( + 'Confirm Booking', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildAppBar(BuildContext context) { + return Container( + color: Colors.white, + child: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + title: Text( + 'Doctor', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ), + ); + } + + Widget _buildDoctorCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDoctorImage(), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.doctor.firstName ?? '', + style: GoogleFonts.poppins( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.doctor.speciality!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.medical_services, + size: 16, color: Colors.blue[400]), + const SizedBox(width: 4), + Expanded( + child: Text( + widget.doctor.speciality!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ], + ), + Row( + children: [ + Icon(Icons.location_on, + size: 16, color: Colors.blue[400]), + const SizedBox(width: 4), + Expanded( + child: Text( + widget.doctor.city!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ], + ), + Row( + children: [ + Icon(Icons.star, size: 16, color: Colors.blue[400]), + const SizedBox(width: 4), + Text( + '${widget.doctor.yearsOfExperience} Years', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDoctorImage() { + final imageProvider = + widget.preloadedImage ?? NetworkImage(widget.doctor.profileImageUrl!); + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image( + image: imageProvider, + width: 100, + height: 100, + fit: BoxFit.cover, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.person, + size: 50, + color: Colors.grey[600], + ), + ); + }, + ), + ); + } + + Widget _buildDescription() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description', + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + widget.doctor.profileDescription!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + maxLines: isDescriptionExpanded ? null : 3, + overflow: isDescriptionExpanded ? null : TextOverflow.ellipsis, + ), + Align( + alignment: Alignment.topLeft, + child: TextButton( + onPressed: () { + setState(() { + isDescriptionExpanded = !isDescriptionExpanded; + }); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + isDescriptionExpanded ? 'Show less' : 'Read more', + style: GoogleFonts.poppins( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildQualifications() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Qualifications', + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + widget.doctor.qualifications?.join(', ') ?? '', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/patient_screens/appoinment_bookings/doctors_list_screen.dart b/lib/screens/patient_screens/appoinment_bookings/doctors_list_screen.dart new file mode 100644 index 0000000..39ba33f --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/doctors_list_screen.dart @@ -0,0 +1,387 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:medora/data/models/doctor.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:medora/route/route_names.dart'; +import 'package:shimmer/shimmer.dart'; + +class DoctorsListScreen extends StatefulWidget { + final String specialty; + + const DoctorsListScreen({ + super.key, + required this.specialty, + }); + + @override + State createState() => _DoctorsListScreenState(); +} + +class _DoctorsListScreenState extends State { + late final Query doctorsQuery; + final TextEditingController _searchController = TextEditingController(); + bool _isSearching = false; + + @override + void initState() { + super.initState(); + + doctorsQuery = FirebaseFirestore.instance + .collection('doctorprofiles') + .where('speciality', isEqualTo: widget.specialty); + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged() { + setState(() { + _isSearching = _searchController.text.isNotEmpty; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + body: CustomScrollView( + slivers: [ + _buildSliverAppBar(), + SliverToBoxAdapter( + child: _buildSearchBar(), + ), + _buildDoctorsList(), + ], + ), + ); + } + + Widget _buildSliverAppBar() { + return SliverAppBar( + expandedHeight: 55, + floating: true, + pinned: true, + stretch: true, + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: FlexibleSpaceBar( + title: Text( + '${widget.specialty} Specialists', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ), + ); + } + + Widget _buildSearchBar() { + return Padding( + padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search doctors...', + hintStyle: GoogleFonts.poppins( + color: Colors.grey, + fontSize: 14, + ), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _isSearching + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + FocusScope.of(context).unfocus(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ); + } + + Widget _buildDoctorsList() { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: StreamBuilder( + stream: doctorsQuery.snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildShimmerDoctorCard(), + ), + ); + } + + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return SliverFillRemaining( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.medical_services_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No doctors available in this specialty', + style: GoogleFonts.poppins( + fontSize: 16, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ); + } + + final doctors = snapshot.data!.docs + .map((doc) => Doctor.fromJson(doc.data() as Map)) + .where((doctor) { + if (_searchController.text.isEmpty) return true; + final searchQuery = _searchController.text.toLowerCase(); + return doctor.firstName!.toLowerCase().contains(searchQuery) || + doctor.city!.toLowerCase().contains(searchQuery); + }).toList(); + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final doctor = doctors[index]; + return _buildDoctorCard(doctor); + }, + childCount: doctors.length, + ), + ); + }, + ), + ); + } + + Widget _buildShimmerDoctorCard() { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 120, + height: 20, + color: Colors.white, + ), + const SizedBox(height: 8), + Container( + width: 150, + height: 16, + color: Colors.white, + ), + const SizedBox(height: 8), + Container( + width: 100, + height: 16, + color: Colors.white, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildDoctorCard(Doctor doctor) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + precacheImage(NetworkImage(doctor.profileImageUrl!), context); + Navigator.pushNamed( + context, + RouteNames.doctorDetailsScreen, + arguments: { + 'doctor': doctor, + 'imageProvider': NetworkImage(doctor.profileImageUrl!), + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _buildDoctorImage(doctor), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + doctor.firstName ?? '', + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + '${doctor.yearsOfExperience!} years experience', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 4), + Expanded( + child: Text( + doctor.city!, + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: Colors.grey[400], + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildDoctorImage(Doctor doctor) { + return Container( + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + doctor.profileImageUrl!, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + color: Colors.white, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[200], + child: Icon( + Icons.person, + size: 40, + color: Colors.grey[400], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/screens/patient_screens/appoinment_bookings/speciality_screen.dart b/lib/screens/patient_screens/appoinment_bookings/speciality_screen.dart new file mode 100644 index 0000000..233b8a8 --- /dev/null +++ b/lib/screens/patient_screens/appoinment_bookings/speciality_screen.dart @@ -0,0 +1,426 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:animate_do/animate_do.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:medora/route/route_names.dart'; + +class Specialty { + final String name; + final IconData icon; + final Color color; + final String description; + + Specialty({ + required this.name, + required this.icon, + required this.color, + required this.description, + }); +} + +class SpecialtyScreen extends StatefulWidget { + const SpecialtyScreen({super.key}); + + @override + State createState() => _SpecialtyScreenState(); +} + +class _SpecialtyScreenState extends State { + final List _allSpecialties = [ + Specialty( + name: 'Pediatric', + icon: Icons.child_care, + color: Colors.blue, + description: 'Medical care for infants, children, and adolescents', + ), + Specialty( + name: 'General Medicine', + icon: Icons.medical_services, + color: Colors.green, + description: + 'Primary healthcare for adults and general medical conditions', + ), + Specialty( + name: 'Family Medicine', + icon: Icons.family_restroom, + color: Colors.teal, + description: 'Comprehensive healthcare for families and individuals', + ), + Specialty( + name: 'Cardiologist', + icon: Icons.favorite, + color: Colors.red, + description: 'Diagnosis and treatment of heart conditions', + ), + Specialty( + name: 'Neurology', + icon: Icons.psychology, + color: Colors.purple, + description: 'Treatment of nervous system disorders', + ), + Specialty( + name: 'Gastroenterology', + icon: Icons.local_hospital, + color: Colors.orange, + description: 'Digestive system disorders and treatment', + ), + Specialty( + name: 'Dermatologist', + icon: Icons.face, + color: Colors.pink, + description: 'Skin, hair, and nail conditions', + ), + Specialty( + name: 'Orthopedic', + icon: Icons.wheelchair_pickup, + color: Colors.indigo, + description: 'Musculoskeletal system and injury treatment', + ), + Specialty( + name: 'Ophthalmology', + icon: Icons.remove_red_eye, + color: Colors.brown, + description: 'Eye care and vision treatment', + ), + Specialty( + name: 'ENT', + icon: Icons.hearing, + color: Colors.cyan, + description: 'Ear, nose, and throat specialist', + ), + Specialty( + name: 'Psychiatry', + icon: Icons.psychology_outlined, + color: Colors.deepPurple, + description: 'Mental health and behavioral disorders', + ), + Specialty( + name: 'Gynecology', + icon: Icons.pregnant_woman, + color: Colors.pinkAccent, + description: "Women's health and reproductive care", + ), + Specialty( + name: 'Urology', + icon: Icons.water_drop, + color: Colors.lightBlue, + description: 'Urinary tract and male reproductive health', + ), + Specialty( + name: 'Endocrinology', + icon: Icons.biotech, + color: Colors.amber, + description: 'Hormone and metabolic disorders', + ), + Specialty( + name: 'Oncology', + icon: Icons.bloodtype, + color: Colors.redAccent, + description: 'Cancer diagnosis and treatment', + ), + Specialty( + name: 'Rheumatology', + icon: Icons.accessibility, + color: Colors.deepOrange, + description: 'Arthritis and autoimmune conditions', + ), + Specialty( + name: 'Pulmonology', + icon: Icons.air, + color: Colors.lightGreen, + description: 'Respiratory system disorders', + ), + Specialty( + name: 'Nephrology', + icon: Icons.water, + color: Colors.blueGrey, + description: 'Kidney diseases and disorders', + ), + Specialty( + name: 'Dentistry', + icon: Icons.cleaning_services, + color: Colors.cyan, + description: 'Oral health and dental care', + ), + Specialty( + name: 'Physical Therapy', + icon: Icons.accessibility_new, + color: Colors.deepPurple, + description: 'Rehabilitation and physical medicine', + ), + Specialty( + name: 'Sports Medicine', + icon: Icons.sports, + color: Colors.green, + description: 'Athletic injuries and performance', + ), + Specialty( + name: 'Allergy & Immunology', + icon: Icons.sick, + color: Colors.orange, + description: 'Allergies and immune system disorders', + ), + Specialty( + name: 'Pain Management', + icon: Icons.healing, + color: Colors.red, + description: 'Chronic pain treatment', + ), + Specialty( + name: 'Sleep Medicine', + icon: Icons.bedtime, + color: Colors.indigo, + description: 'Sleep disorders and treatment', + ), + Specialty( + name: 'Geriatrics', + icon: Icons.elderly, + color: Colors.brown, + description: 'Healthcare for elderly patients', + ), + ]; + + late List _filteredSpecialties; + final TextEditingController _searchController = TextEditingController(); + bool _isSearching = false; + + @override + void initState() { + super.initState(); + _filteredSpecialties = _allSpecialties; + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged() { + final searchQuery = _searchController.text.toLowerCase(); + setState(() { + _isSearching = searchQuery.isNotEmpty; + _filteredSpecialties = _allSpecialties + .where((specialty) => + specialty.name.toLowerCase().contains(searchQuery) || + specialty.description.toLowerCase().contains(searchQuery)) + .toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FF), + body: CustomScrollView( + slivers: [ + _buildSliverAppBar(), + SliverToBoxAdapter( + child: _buildSearchBar(), + ), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: _buildSpecialtiesGrid(), + ), + ], + ), + ); + } + + Widget _buildSliverAppBar() { + return SliverAppBar( + expandedHeight: 55, + floating: true, + pinned: true, + stretch: true, + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black87), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: FlexibleSpaceBar( + title: Text( + 'Find a Specialist', + style: GoogleFonts.poppins( + color: Colors.black87, + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + centerTitle: true, + ), + ); + } + + Widget _buildSearchBar() { + return FadeIn( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search specialties...', + hintStyle: GoogleFonts.poppins( + color: Colors.grey, + fontSize: 14, + ), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _isSearching + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + FocusScope.of(context).unfocus(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.white, + ), + ), + ), + ), + ); + } + + Widget _buildSpecialtiesGrid() { + return SliverAnimationBuilder( + child: MasonryGridView.count( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _filteredSpecialties.length, + itemBuilder: (context, index) { + final specialty = _filteredSpecialties[index]; + return FadeInUp( + delay: Duration(milliseconds: 100 * index), + child: _buildSpecialtyCard(specialty), + ); + }, + ), + ); + } + + Widget _buildSpecialtyCard(Specialty specialty) { + return GestureDetector( + onTap: () { + Navigator.pushNamed( + context, + RouteNames.doctorListScreen, + arguments: { + 'specialty': specialty.name, + }, + ); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: specialty.color.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 5), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + children: [ + Positioned( + right: -20, + top: -20, + child: CircleAvatar( + radius: 40, + backgroundColor: specialty.color.withOpacity(0.1), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: specialty.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + specialty.icon, + color: specialty.color, + size: 28, + ), + ), + const SizedBox(height: 16), + Text( + specialty.name, + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + specialty.description, + style: GoogleFonts.poppins( + fontSize: 12, + color: Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class SliverAnimationBuilder extends StatelessWidget { + final Widget child; + + const SliverAnimationBuilder({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return SliverAnimatedList( + initialItemCount: 1, + itemBuilder: (context, index, animation) { + return SlideInUp( + from: 50, + child: child, + ); + }, + ); + } +} diff --git a/lib/screens/patient_screens/patient_dashboard/patient_dashboard_screen.dart b/lib/screens/patient_screens/patient_dashboard/patient_dashboard_screen.dart new file mode 100644 index 0000000..9b82a9b --- /dev/null +++ b/lib/screens/patient_screens/patient_dashboard/patient_dashboard_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:animations/animations.dart'; +import 'package:curved_navigation_bar/curved_navigation_bar.dart'; +import 'package:medora/screens/patient_screens/patient_dashboard/patient_home_screen.dart'; +import 'package:medora/screens/patient_screens/patient_dashboard/patient_profile_screen.dart'; + +class PatientDashboardScreen extends StatefulWidget { + const PatientDashboardScreen({super.key}); + + @override + State createState() => _PatientDashboardScreenState(); +} + +class _PatientDashboardScreenState extends State { + int _selectedIndex = 0; + final GlobalKey _bottomNavigationKey = GlobalKey(); + + // Add your pages here + final List _pages = [ + const PatientHomeScreen(), + const Center(child: Text('Chat')), // Replace with your chat screen + const Center(child: Text('Records')), // Replace with your records screen + const PatientProfileScreen(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: PageTransitionSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation, secondaryAnimation) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + child: _pages[_selectedIndex], + ), + bottomNavigationBar: CurvedNavigationBar( + key: _bottomNavigationKey, + backgroundColor: Colors.transparent, + color: Colors.blue, + buttonBackgroundColor: Colors.blue, + height: 60, + index: _selectedIndex, + items: const [ + Icon(Icons.home, size: 30, color: Colors.white), + Icon(Icons.chat_bubble, size: 30, color: Colors.white), + Icon(Icons.assignment, size: 30, color: Colors.white), + Icon(Icons.person, size: 30, color: Colors.white), + ], + onTap: (index) { + setState(() { + _selectedIndex = index; + }); + }, + ), + ); + } +} diff --git a/lib/screens/patient_screens/patient_dashboard/patient_home_screen.dart b/lib/screens/patient_screens/patient_dashboard/patient_home_screen.dart new file mode 100644 index 0000000..165b283 --- /dev/null +++ b/lib/screens/patient_screens/patient_dashboard/patient_home_screen.dart @@ -0,0 +1,695 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; + +import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; +import 'package:medora/data/models/consultation_booking.dart'; +import 'package:medora/data/services/consultation_booking_service.dart'; +import 'package:medora/route/route_names.dart'; +import 'package:medora/screens/patient_screens/appoinment_bookings/speciality_screen.dart'; + +class PatientHomeScreen extends StatefulWidget { + const PatientHomeScreen({super.key}); + + @override + State createState() => _PatientHomeScreenState(); +} + +class _PatientHomeScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + final BookingService _bookingService = BookingService(); + late Stream> _bookingsStream; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _animationController.forward(); + + final User? user = FirebaseAuth.instance.currentUser; + if (user != null) { + final String userId = user.uid; + _bookingsStream = _bookingService.getPatientBookings(userId); + } else { + _bookingsStream = const Stream.empty(); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildRealTimeCard(), + const SizedBox(height: 20), + _buildConsultationsSection(), + const SizedBox(height: 20), + _buildFindDoctorSection(), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSearchBar() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30.0), + bottomRight: Radius.circular(30.0), + ), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: TextField( + decoration: InputDecoration( + hintText: 'Search Doctor/Hospital/Symptoms', + hintStyle: GoogleFonts.poppins( + color: Colors.grey[400], + ), + prefixIcon: const Icon(Icons.search, color: Colors.blue), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + ), + ), + ), + ], + ), + ); + } + + Widget _buildRealTimeCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue[400]!, Colors.white], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Real-time care\nat your fingertips.', + style: GoogleFonts.poppins( + fontSize: 30, + fontWeight: FontWeight.bold, + color: const Color.fromARGB(221, 67, 67, 67), + ), + ), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SpecialtyScreen()), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.blue[700], + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 5, + ), + child: Text( + 'Start Consultation', + style: GoogleFonts.poppins( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } + + Widget _buildConsultationsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Upcoming Consultations', + style: GoogleFonts.poppins( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + SizedBox( + height: 201, + child: StreamBuilder>( + stream: _bookingsStream, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 12), + Text( + 'Loading consultations...', + style: GoogleFonts.poppins( + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, + color: Colors.red[400], size: 48), + const SizedBox(height: 12), + Text( + 'Error loading consultations', + style: GoogleFonts.poppins( + color: Colors.red[400], + fontWeight: FontWeight.w500, + ), + ), + TextButton( + onPressed: () { + // Implement refresh logic + }, + child: Text( + 'Try Again', + style: GoogleFonts.poppins( + color: Colors.blue[700], + ), + ), + ), + ], + ), + ); + } + + final bookings = snapshot.data ?? []; + + if (bookings.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.calendar_today, + color: Colors.grey[400], size: 48), + const SizedBox(height: 12), + Text( + 'No upcoming consultations', + style: GoogleFonts.poppins( + color: Colors.grey[600], + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + // Navigate to book consultation + }, + child: Text( + 'Book a Consultation', + style: GoogleFonts.poppins( + color: Colors.blue[700], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 20), + scrollDirection: Axis.horizontal, + itemCount: bookings.length, + itemBuilder: (context, index) { + final booking = bookings[index]; + return Padding( + padding: const EdgeInsets.only(right: 16), + child: Hero( + tag: 'consultation_${booking.id}', + child: Material( + child: _consultationCard( + booking.profileImageUrl, + booking.doctorName, + '${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}', + booking.specialization, + booking.paymentStatus, + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ); + } + + Widget _consultationCard( + String? profileImageUrl, + String name, + String schedule, + String speciality, + PaymentStatus paymentStatus, + ) { + return Container( + width: 300, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.grey[50]!], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (profileImageUrl != null) + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: NetworkImage(profileImageUrl), + fit: BoxFit.cover, + ), + boxShadow: [ + BoxShadow( + color: Colors.blue[300]!.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + ) + else + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue[400]!, Colors.blue[600]!], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.blue[300]!.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.person, + size: 36, + color: Colors.white, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + Text( + speciality, + style: GoogleFonts.poppins( + color: Colors.grey[600], + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getStatusColor(paymentStatus).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: + _getStatusColor(paymentStatus).withOpacity(0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getStatusIcon(paymentStatus), + size: 14, + color: _getStatusColor(paymentStatus), + ), + const SizedBox(width: 4), + Text( + _getStatusText(paymentStatus), + style: GoogleFonts.poppins( + fontSize: 12, + color: _getStatusColor(paymentStatus), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.blue[100]!, + ), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today, + size: 18, + color: Colors.blue[700], + ), + const SizedBox(width: 8), + Text( + schedule, + style: GoogleFonts.poppins( + color: Colors.blue[700], + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } + + IconData _getStatusIcon(PaymentStatus status) { + switch (status) { + case PaymentStatus.completed: + return Icons.check_circle; + case PaymentStatus.pending: + return Icons.access_time; + case PaymentStatus.failed: + return Icons.error; + default: + return Icons.info; + } + } + + Color _getStatusColor(PaymentStatus status) { + switch (status) { + case PaymentStatus.pending: + return Colors.orange; + case PaymentStatus.completed: + return Colors.green; + case PaymentStatus.failed: + return Colors.red; + default: + return Colors.grey; + } + } + + String _getStatusText(PaymentStatus status) { + switch (status) { + case PaymentStatus.pending: + return 'Payment Pending'; + case PaymentStatus.completed: + return 'Confirmed'; + case PaymentStatus.failed: + return 'Payment Failed'; + default: + return 'Unknown'; + } + } + + Widget _buildFindDoctorSection() { + final specialistData = [ + { + 'icon': Icons.local_hospital, + 'label': 'General', + 'color': Colors.blue, + 'description': 'Primary Healthcare' + }, + { + 'icon': Icons.remove_red_eye, + 'label': 'Eye', + 'color': Colors.indigo, + 'description': 'Vision Care' + }, + { + 'icon': Icons.medical_services, + 'label': 'Dental', + 'color': Colors.amber, + 'description': 'Oral Health' + }, + { + 'icon': Icons.favorite, + 'label': 'Cardio', + 'color': Colors.red, + 'description': 'Heart Specialist' + }, + { + 'icon': Icons.psychology, + 'label': 'Mental', + 'color': Colors.green, + 'description': 'Mental Health' + }, + { + 'icon': Icons.child_care, + 'label': 'Pediatric', + 'color': Colors.purple, + 'description': 'Child Care' + }, + { + 'icon': Icons.elderly, + 'label': 'Geriatric', + 'color': Colors.teal, + 'description': 'Senior Care' + }, + { + 'icon': Icons.fitness_center, + 'label': 'Physio', + 'color': Colors.orange, + 'description': 'Physical Therapy' + }, + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + 'Find Specialists', + style: GoogleFonts.poppins( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (final data in specialistData) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _specialistCard( + icon: data['icon'] as IconData, + label: data['label'] as String, + color: data['color'] as Color, + description: data['description'] as String, + ), + ), + const SizedBox(width: 8), + ], + ), + ), + ), + IconButton( + icon: const Icon(Icons.arrow_forward, color: Colors.blue), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SpecialtyScreen()), + ); + }, + ), + ], + ), + ], + ); + } + + Widget _specialistCard({ + required IconData icon, + required String label, + required Color color, + required String description, + }) { + return GestureDetector( + onTap: () { + Navigator.pushNamed( + context, + RouteNames.doctorListScreen, + arguments: { + 'specialty': label, + }, + ); + }, + child: Container( + width: 140, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 24), + ), + const SizedBox(height: 8), + Text( + label, + style: GoogleFonts.poppins( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + description, + style: GoogleFonts.poppins( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/patient_screens/patient_dashboard/patient_profile_screen.dart b/lib/screens/patient_screens/patient_dashboard/patient_profile_screen.dart new file mode 100644 index 0000000..703f866 --- /dev/null +++ b/lib/screens/patient_screens/patient_dashboard/patient_profile_screen.dart @@ -0,0 +1,197 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:medora/data/services/patient_registration_service.dart'; +import 'package:medora/route/route_names.dart'; +import 'package:medora/data/models/patient.dart'; + +class PatientProfileScreen extends StatefulWidget { + const PatientProfileScreen({super.key}); + + @override + State createState() => _PatientProfileScreenState(); +} + +class _PatientProfileScreenState extends State { + final FirebaseAuth _auth = FirebaseAuth.instance; + PatientModel? _patientProfile; + + @override + void initState() { + super.initState(); + _fetchPatientProfile(); + } + + Future _fetchPatientProfile() async { + final patientProfile = await PatientProfileService.getPatientProfile(); + if (mounted) { + setState(() { + _patientProfile = patientProfile; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + _buildProfileHeader(), + _buildProfileOptions(), + ], + ), + ), + ); + } + + Widget _buildProfileHeader() { + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF00BCD4), + Color(0xFF2196F3), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + image: _patientProfile?.profileImageUrl != null + ? DecorationImage( + image: NetworkImage(_patientProfile!.profileImageUrl!), + fit: BoxFit.cover, + ) + : null, + ), + child: _patientProfile?.profileImageUrl == null + ? Center( + child: Text( + _patientProfile != null && _patientProfile!.name != null + ? _patientProfile!.name![0].toUpperCase() + : '', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ) + : null, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _patientProfile != null && _patientProfile!.name != null + ? _patientProfile!.name! + : 'Create your profile', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right, + color: Colors.white, + size: 30, + ), + ], + ), + ); + } + + Widget _buildProfileOptions() { + return Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + ), + ], + ), + child: Column( + children: [ + _buildOptionTile( + 'Medical Profile', + Icons.medical_information_outlined, + onTap: () { + // Add navigation or action + }, + ), + const Divider(height: 1), + _buildOptionTile( + 'Sign Out', + Icons.logout, + onTap: () { + _signOut(); + }, + iconColor: Colors.blue, + ), + ], + ), + ); + } + + Widget _buildOptionTile(String title, IconData icon, + {required VoidCallback onTap, Color? iconColor}) { + return ListTile( + leading: Icon( + icon, + color: iconColor ?? Colors.grey, + size: 24, + ), + title: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + trailing: const Icon( + Icons.chevron_right, + color: Colors.grey, + ), + onTap: onTap, + ); + } + + Future _signOut() async { + try { + await _auth.signOut(); + if (mounted) { + Navigator.of(context).pushReplacementNamed(RouteNames.launch); + } + } catch (e) { + print("Error signing out: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to log out. Please try again.')), + ); + } + } + } +} diff --git a/lib/screens/patient_screens/patient_landing_screen.dart b/lib/screens/patient_screens/patient_landing_screen.dart new file mode 100644 index 0000000..f3cda12 --- /dev/null +++ b/lib/screens/patient_screens/patient_landing_screen.dart @@ -0,0 +1,94 @@ +import 'package:medora/route/route_names.dart'; +import 'package:flutter/material.dart'; + +class PatientLandingScreen extends StatelessWidget { + const PatientLandingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.teal.shade100, Colors.white], + ), + ), + child: SafeArea( + child: Column( + children: [ + Expanded( + child: Center( + child: Card( + margin: const EdgeInsets.symmetric(horizontal: 32), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.topRight, + child: TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + RouteNames.patientDashboardScreen); + }, + child: Text( + 'Skip', + style: TextStyle( + color: Colors.teal.shade300, + fontSize: 16, + fontWeight: FontWeight.bold), + ), + ), + ), + Image.asset( + 'images/patient-avathar.png', + height: 200, + width: 200, + ), + const SizedBox(height: 24), + const Text( + 'Set your medical profile', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + Navigator.of(context).pushNamed( + RouteNames.patientRegistrationScreen); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + minimumSize: const Size(double.infinity, 50), + ), + child: const Text( + 'Continue', + style: + TextStyle(fontSize: 18, color: Colors.white), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/patient_screens/registration_screens/family_members_edit_screen.dart b/lib/screens/patient_screens/registration_screens/family_members_edit_screen.dart new file mode 100644 index 0000000..1692af9 --- /dev/null +++ b/lib/screens/patient_screens/registration_screens/family_members_edit_screen.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:medora/controllers/patient_controller.dart'; +import 'package:medora/data/models/patient.dart'; + +class FamilyMembersEditScreen extends StatefulWidget { + final FamilyMember? familyMember; + final PatientController controller; + + const FamilyMembersEditScreen( + {super.key, this.familyMember, required this.controller}); + + @override + State createState() => + _FamilyMembersEditScreenState(); +} + +class _FamilyMembersEditScreenState extends State { + late TextEditingController nameController; + late TextEditingController relationController; + late TextEditingController genderController; + late TextEditingController dobController; + Map errors = {}; + + @override + void initState() { + super.initState(); + nameController = + TextEditingController(text: widget.familyMember?.name ?? ''); + relationController = + TextEditingController(text: widget.familyMember?.relation ?? ''); + genderController = + TextEditingController(text: widget.familyMember?.gender ?? ''); + dobController = TextEditingController( + text: widget.familyMember?.dateOfBirth?.toString().split(' ')[0] ?? ''); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Edit Family Member'), + actions: _buildAppBarActions(), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTextField(nameController, 'Name', Icons.person, 'name'), + _buildDropdownField( + 'Relation', + relationController.text, + (String? newValue) { + setState(() { + relationController.text = newValue ?? ''; + }); + }, + Icons.family_restroom, + ), + _buildDropdownField( + 'Gender', + genderController.text, + (String? newValue) { + setState(() { + genderController.text = newValue ?? ''; + }); + }, + Icons.transgender, + ), + _buildDateField(context), + ], + ), + ), + ); + } + + Widget _buildDropdownField( + String label, String value, Function(String?) onChanged, IconData icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: Colors.blue), + border: const OutlineInputBorder(), + ), + value: value.isEmpty ? null : value, + onChanged: onChanged, + items: label == 'Relation' + ? ['Father', 'Mother', 'Son', 'Daughter', 'Other'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList() + : ['Male', 'Female', 'Other'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ); + } + + Widget _buildDateField(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: TextField( + controller: dobController, + decoration: const InputDecoration( + labelText: 'Date of Birth', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.calendar_today, color: Colors.blue), + ), + readOnly: true, + onTap: () async { + DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now().subtract(const Duration(days: 365)), + firstDate: DateTime(1900), + lastDate: DateTime.now().subtract(const Duration(days: 365)), + ); + if (pickedDate != null) { + setState(() { + dobController.text = pickedDate.toString().split(' ')[0]; + }); + } + }, + ), + ); + } + + bool _validateFields() { + errors.clear(); + + if (nameController.text.trim().isEmpty) { + errors['name'] = 'Name is required'; + } else if (nameController.text.trim().length < 2) { + errors['name'] = 'Name must be at least 2 characters'; + } + + if (relationController.text.isEmpty) { + errors['relation'] = 'Please select a relation'; + } + + if (genderController.text.isEmpty) { + errors['gender'] = 'Please select a gender'; + } + + if (dobController.text.isEmpty) { + errors['dob'] = 'Date of Birth is required'; + } else { + final dob = DateTime.tryParse(dobController.text); + if (dob == null) { + errors['dob'] = 'Invalid date format'; + } else if (dob.isAfter(DateTime.now())) { + errors['dob'] = 'Date of Birth cannot be in the future'; + } + } + + setState(() {}); + return errors.isEmpty; + } + + void _showValidationErrors() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 8), + Text('Validation Errors'), + ], + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: errors.entries + .map((error) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + '• ${error.value}', + style: const TextStyle(color: Colors.red), + ), + )) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + Widget _buildTextField( + TextEditingController controller, + String label, + IconData icon, + String errorKey, + ) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon( + icon, + color: errors.containsKey(errorKey) ? Colors.red : Colors.blue, + ), + border: OutlineInputBorder( + borderSide: BorderSide( + color: + errors.containsKey(errorKey) ? Colors.red : Colors.grey, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: + errors.containsKey(errorKey) ? Colors.red : Colors.grey, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: + errors.containsKey(errorKey) ? Colors.red : Colors.blue, + ), + ), + ), + ), + if (errors.containsKey(errorKey)) + Padding( + padding: const EdgeInsets.only(top: 4, left: 12), + child: Text( + errors[errorKey]!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ], + ), + ); + } + + List _buildAppBarActions() { + return [ + TextButton( + onPressed: () { + if (_validateFields()) { + FamilyMember newMember = FamilyMember( + name: nameController.text, + relation: relationController.text, + gender: genderController.text, + dateOfBirth: DateTime.tryParse(dobController.text), + ); + Navigator.pop(context, newMember); + } else { + _showValidationErrors(); + } + }, + child: const Text('Done', style: TextStyle(color: Colors.blue)), + ), + ]; + } + + @override + void dispose() { + nameController.dispose(); + relationController.dispose(); + genderController.dispose(); + dobController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/patient_screens/registration_screens/patient_adress_screen.dart b/lib/screens/patient_screens/registration_screens/patient_adress_screen.dart new file mode 100644 index 0000000..a69441a --- /dev/null +++ b/lib/screens/patient_screens/registration_screens/patient_adress_screen.dart @@ -0,0 +1,333 @@ +import 'package:medora/controllers/patient_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:country_state_city_picker/country_state_city_picker.dart'; + +class PatientAddressScreen extends StatefulWidget { + final PatientController? controller; + + const PatientAddressScreen({super.key, required this.controller}); + + @override + State createState() => _PatientAddressScreenState(); +} + +class _PatientAddressScreenState extends State { + late PatientController _controller; + late TextEditingController _houseNoController; + late TextEditingController _lineController; + late TextEditingController _townController; + late TextEditingController _pincodeController; + final String country = 'India'; + String? state; + String? city; + String? addressType; + final Map _errors = {}; + bool _hasErrors = false; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? PatientController(); + _loadSavedData(); + } + + void _loadSavedData() { + final address = _controller.model.address; + _houseNoController = TextEditingController(text: address.houseNo ?? ''); + _lineController = TextEditingController(text: address.line ?? ''); + _townController = TextEditingController(text: address.town ?? ''); + _pincodeController = TextEditingController(text: address.pincode ?? ''); + + state = address.state; + city = address.city; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Address'), + actions: [ + TextButton( + onPressed: _saveAndExit, + child: const Text('Done', style: TextStyle(color: Colors.blue)), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionContainer( + 'Address Information', + Column( + children: [ + _buildTextField( + 'House No.', + _houseNoController, + (value) => widget.controller!.updateHouseNo(value), + icon: Icons.home_outlined, + errorKey: 'houseNo', + ), + _buildTextField( + 'Address Line', + _lineController, + (value) => widget.controller!.updateLine(value), + icon: Icons.location_on_outlined, + errorKey: 'line', + ), + _buildTextField( + 'Town (Optional)', + _townController, + (value) => widget.controller!.updateTown(value), + icon: Icons.location_city_outlined, + ), + _buildTextField( + 'Pincode', + _pincodeController, + (value) => widget.controller!.updatePincode(value), + icon: Icons.pin_drop_outlined, + errorKey: 'pincode', + ), + ], + ), + ), + const SizedBox(height: 20), + _buildSectionContainer( + 'Location', + Column( + children: [ + _buildCountrySelection(), + const SizedBox(height: 10), + SelectState( + onCountryChanged: (value) { + setState(() {}); + widget.controller!.updateCountry('India'); + }, + onStateChanged: (value) { + setState(() { + state = value; + }); + widget.controller!.updateState(value); + }, + onCityChanged: (value) { + setState(() { + city = value; + }); + widget.controller!.updateCity(value); + }, + ), + const SizedBox(height: 20), + if (state != null) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text('State: $state', + style: const TextStyle( + fontSize: 14, color: Colors.black87)), + ), + if (city != null) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text('City: $city', + style: const TextStyle( + fontSize: 14, color: Colors.black87)), + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + bool _validateFields() { + setState(() { + _errors.clear(); + _hasErrors = false; + + if (_houseNoController.text.trim().isEmpty) { + _errors['houseNo'] = 'House No. is required'; + _hasErrors = true; + } + + if (_lineController.text.trim().isEmpty) { + _errors['line'] = 'Address Line is required'; + _hasErrors = true; + } + + final pincode = _pincodeController.text.trim(); + if (pincode.isEmpty) { + _errors['pincode'] = 'Pincode is required'; + _hasErrors = true; + } else if (!RegExp(r'^[1-9][0-9]{5}$').hasMatch(pincode)) { + _errors['pincode'] = 'Enter a valid 6-digit pincode'; + _hasErrors = true; + } + + if (state == null || state!.isEmpty) { + _errors['state'] = 'State is required'; + _hasErrors = true; + } + + if (city == null || city!.isEmpty) { + _errors['city'] = 'City is required'; + _hasErrors = true; + } + }); + + return !_hasErrors; + } + + Widget _buildSectionContainer(String title, Widget content) { + return Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + content, + ], + ), + ); + } + + Widget _buildTextField( + String label, + TextEditingController controller, + Function(String) onChanged, { + required IconData icon, + String? errorKey, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, + color: _errors.containsKey(errorKey) + ? Colors.red + : Colors.blueAccent), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _errors.containsKey(errorKey) ? Colors.red : Colors.grey, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _errors.containsKey(errorKey) + ? Colors.red + : Colors.blueAccent, + ), + ), + errorText: _errors[errorKey], + ), + onChanged: onChanged, + ), + const SizedBox(height: 20), + ], + ); + } + + Widget _buildCountrySelection() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: const Row( + children: [ + Text( + 'Country:', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + SizedBox(width: 8), + Text('India', style: TextStyle(fontSize: 16)), + ], + ), + ); + } + + void _saveAndExit() { + if (_validateFields()) { + widget.controller!.updateHouseNo(_houseNoController.text); + widget.controller!.updateLine(_lineController.text); + widget.controller!.updateTown(_townController.text); + widget.controller!.updatePincode(_pincodeController.text); + widget.controller!.updateCountry(country); + widget.controller!.updateState(state ?? ''); + widget.controller!.updateCity(city ?? ''); + + widget.controller!.updatePatientData(); + Navigator.pop(context, true); + } else { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 8), + Text('Validation Errors'), + ], + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: _errors.entries + .map((error) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + '• ${error.value}', + style: const TextStyle(color: Colors.red), + ), + )) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + } + + @override + void dispose() { + _houseNoController.dispose(); + _lineController.dispose(); + _townController.dispose(); + _pincodeController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/patient_screens/registration_screens/patient_family_members_screen.dart b/lib/screens/patient_screens/registration_screens/patient_family_members_screen.dart new file mode 100644 index 0000000..8798934 --- /dev/null +++ b/lib/screens/patient_screens/registration_screens/patient_family_members_screen.dart @@ -0,0 +1,308 @@ +import 'package:medora/data/models/patient.dart'; +import 'package:flutter/material.dart'; +import 'package:medora/screens/patient_screens/registration_screens/family_members_edit_screen.dart'; +import '../../../controllers/patient_controller.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class PatientFamilyMembersScreen extends StatefulWidget { + final PatientController controller; + const PatientFamilyMembersScreen({ + super.key, + required this.controller, + }); + + @override + State createState() => + _PatientFamilyMembersScreenState(); +} + +class _PatientFamilyMembersScreenState + extends State { + bool isLoading = false; + final int maxFamilyMembers = 5; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Family Members', + style: TextStyle(fontSize: 20), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () { + if (_validateFamilyMembers()) { + Navigator.pop(context); + } + }, + ), + actions: _buildAppBarActions(), + elevation: 0, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: widget.controller.model.familyMembers.length, + itemBuilder: (context, index) { + return FamilyMemberCard( + familyMember: widget.controller.model.familyMembers[index], + onEdit: () => _editFamilyMember(index), + onDelete: () => _deleteFamilyMember(index), + ); + }, + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _addFamilyMember, + backgroundColor: Colors.blue, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + List _buildAppBarActions() { + return [ + TextButton( + onPressed: () { + if (_validateFamilyMembers()) { + Navigator.pop(context); + } + }, + child: const Text( + 'Done', + style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold), + ), + ), + ]; + } + + bool _validateFamilyMembers() { + if (widget.controller.model.familyMembers.isEmpty) { + _showValidationError('Please add at least one family member'); + return false; + } + + final relations = widget.controller.model.familyMembers + .map((member) => member.relation?.toLowerCase()) + .toList(); + if (relations.toSet().length != relations.length) { + _showValidationError('Duplicate relations are not allowed'); + return false; + } + + return true; + } + + void _showValidationError(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 8), + Text('Validation Error'), + ], + ), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + void _addFamilyMember() { + if (widget.controller.model.familyMembers.length >= maxFamilyMembers) { + _showValidationError('Maximum $maxFamilyMembers family members allowed'); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FamilyMembersEditScreen( + controller: widget.controller, + ), + ), + ).then((newMember) { + if (newMember != null) { + setState(() { + widget.controller.addFamilyMember(newMember); + }); + } + }); + } + + void _editFamilyMember(int index) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FamilyMembersEditScreen( + controller: widget.controller, + familyMember: widget.controller.model.familyMembers[index], + ), + ), + ).then((editedMember) { + if (editedMember != null) { + setState(() { + widget.controller.updateFamilyMember(index, editedMember); + }); + } + }); + } + + void _deleteFamilyMember(int index) { + if (widget.controller.model.familyMembers.length <= 1) { + _showValidationError('At least one family member is required'); + return; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Family Member'), + content: + const Text('Are you sure you want to delete this family member?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + setState(() { + widget.controller.deleteFamilyMember(index); + }); + Navigator.pop(context); + }, + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } +} + +class FamilyMemberCard extends StatelessWidget { + final FamilyMember familyMember; + final VoidCallback onEdit; + final VoidCallback onDelete; + + const FamilyMemberCard({ + super.key, + required this.familyMember, + required this.onEdit, + required this.onDelete, + }); + + Widget _buildInfoRow(IconData icon, String label, String? value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: Colors.blueGrey), + const SizedBox(width: 6), + Text( + label, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + value ?? 'Not provided', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: value == null || value.isEmpty + ? Colors.redAccent + : Colors.black87, + fontStyle: value == null || value.isEmpty + ? FontStyle.italic + : FontStyle.normal, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Slidable( + key: ValueKey(familyMember), + endActionPane: ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (context) => onEdit(), + foregroundColor: Colors.blue, + icon: Icons.edit, + padding: EdgeInsets.zero, + spacing: 0, + ), + SlidableAction( + onPressed: (context) => onDelete(), + foregroundColor: Colors.red, + icon: Icons.delete, + padding: EdgeInsets.zero, + spacing: 0, + ), + ], + ), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.blueGrey[50], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildInfoRow(Icons.person, 'Name:', familyMember.name), + const SizedBox(height: 10), + _buildInfoRow( + Icons.transgender, 'Gender:', familyMember.gender), + const SizedBox(height: 10), + _buildInfoRow( + Icons.cake, + 'Date of Birth:', + familyMember.dateOfBirth?.toString().split(' ')[0] ?? + 'Not provided', + ), + const SizedBox(height: 10), + _buildInfoRow( + Icons.family_restroom, 'Relation:', familyMember.relation), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/patient_screens/registration_screens/patient_registration_screen.dart b/lib/screens/patient_screens/registration_screens/patient_registration_screen.dart new file mode 100644 index 0000000..1cacbe2 --- /dev/null +++ b/lib/screens/patient_screens/registration_screens/patient_registration_screen.dart @@ -0,0 +1,689 @@ +import 'package:medora/route/route_names.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'dart:io'; +import '../../../controllers/patient_controller.dart'; +import '../../../widgets/alert_screen.dart'; + +class PatientRegistrationScreen extends StatefulWidget { + const PatientRegistrationScreen({super.key}); + + @override + State createState() => + _PatientRegistrationScreenState(); +} + +class _PatientRegistrationScreenState extends State { + final PatientController _controller = PatientController(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _phoneController = TextEditingController(); + bool _hasErrors = false; + final Map _errors = {}; + + String? _gender; + DateTime? _dateOfBirth; + File? _image; + final ImagePicker _picker = ImagePicker(); + String _selectedCountryCode = '+1'; + + final List _countryCodes = ['+1', '+91', '+44', '+61', '+81']; + @override + void initState() { + super.initState(); + _nameController.text = _controller.model.name ?? ''; + if (_controller.model.phoneNumber != null) { + String phoneNumber = _controller.model.phoneNumber!; + if (phoneNumber.startsWith('+')) { + for (String code in _countryCodes) { + if (phoneNumber.startsWith(code)) { + _selectedCountryCode = code; + _phoneController.text = phoneNumber.substring(code.length); + break; + } + } + } else { + _phoneController.text = phoneNumber; + } + } + + _gender = _controller.model.gender; + _dateOfBirth = _controller.model.dateOfBirth; + if (_controller.model.profileImagePath != null) { + _image = File(_controller.model.profileImagePath!); + } + _updateCombinedPhoneNumber(_phoneController.text); + } + + Future _getImage(ImageSource source) async { + final XFile? pickedFile = await _picker.pickImage(source: source); + + if (pickedFile != null) { + setState(() { + _image = File(pickedFile.path); + }); + _controller.updateProfileImage(pickedFile.path); + } + } + + void _updateCombinedPhoneNumber(String phoneNumber) { + String cleanPhoneNumber = phoneNumber.replaceAll(RegExp(r'^\+\d{1,3}'), ''); + String fullPhoneNumber = '$_selectedCountryCode$cleanPhoneNumber'; + _controller.updatePhoneNumber(fullPhoneNumber); + } + + void _showImageSourceActionSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text( + 'Select Image Source', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.photo_library, color: Colors.blue), + ), + title: const Text('Choose from Gallery'), + onTap: () { + _getImage(ImageSource.gallery); + Navigator.pop(context); + }, + ), + ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.photo_camera, color: Colors.blue), + ), + title: const Text('Take a Photo'), + onTap: () { + _getImage(ImageSource.camera); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + }, + ); + } + + void _showResultDialog(bool isSuccess) { + if (!mounted) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AlertScreen( + arguments: AlertArguments( + title: isSuccess ? 'Thank You' : 'Oops!', + message: isSuccess + ? 'Profile created successfully!' + : 'Failed to create profile. Please try again.', + actionTitle: isSuccess ? 'Go to Dashboard' : 'Try Again', + type: isSuccess ? AlertType.success : AlertType.error, + onActionPressed: () { + Navigator.pop(context); + if (isSuccess) { + Navigator.pushReplacementNamed( + context, + RouteNames.patientDashboardScreen, + ); + } + }, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + title: const Text( + 'Create Profile', + style: TextStyle(color: Colors.black), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: _handleProfileSave, + icon: const Icon(Icons.check, color: Colors.blue, weight: 50), + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + GestureDetector( + onTap: () => _showImageSourceActionSheet(context), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + shape: BoxShape.circle, + border: Border.all(color: Colors.blue, width: 2), + ), + child: CircleAvatar( + radius: 75, + backgroundImage: + _image != null ? FileImage(_image!) : null, + child: _image == null + ? const Icon(Icons.person, + size: 50, color: Colors.blue) + : null, + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: const Icon(Icons.camera_alt, + size: 20, color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildUniformField( + label: 'Name', + icon: Icons.person_outline, + child: TextField( + controller: _nameController, + onChanged: (value) => _controller.updateName(value), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Enter your name', + ), + ), + ), + _buildUniformField( + label: 'Phone Number', + icon: Icons.phone_outlined, + child: Row( + children: [ + DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedCountryCode, + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedCountryCode = newValue; + }); + + _updateCombinedPhoneNumber( + _phoneController.text); + } + }, + items: + _countryCodes.map>( + (String code) { + return DropdownMenuItem( + value: code, + child: Text(code), + ); + }, + ).toList(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _phoneController, + onChanged: (value) { + _updateCombinedPhoneNumber(value); + }, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Enter your phone number', + ), + ), + ), + ], + ), + ), + _buildUniformField( + label: 'Gender', + icon: Icons.people_outline, + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _gender, + isExpanded: true, + hint: const Text('Select gender'), + onChanged: (value) { + setState(() => _gender = value); + _controller.updateGender(value!); + }, + items: ['Male', 'Female', 'Other'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ), + _buildUniformField( + label: 'Date of Birth', + icon: Icons.calendar_today_outlined, + child: InkWell( + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _dateOfBirth ?? + DateTime.now() + .subtract(const Duration(days: 365 * 18)), + firstDate: DateTime(1900), + lastDate: DateTime.now() + .subtract(const Duration(days: 365 * 18)), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: Colors.blue), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _dateOfBirth) { + setState(() => _dateOfBirth = picked); + _controller.updateDateOfBirth(picked); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + _dateOfBirth != null + ? DateFormat('dd/MM/yyyy').format(_dateOfBirth!) + : 'Select date of birth', + style: TextStyle( + color: _dateOfBirth != null + ? Colors.black87 + : Colors.grey, + ), + ), + ), + ), + ), + ], + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.blueGrey.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildNavigationField( + 'Address', + Icons.location_on, + () async { + final result = await Navigator.pushNamed( + context, + RouteNames.patientAdressScreen, + arguments: _controller, + ); + if (result == true) { + setState(() {}); + } + }, + ), + const Divider(height: 1), + _buildNavigationField( + 'Family Members', + Icons.family_restroom_outlined, + () => Navigator.pushNamed( + context, + RouteNames.patientFamilyMembersScreen, + arguments: _controller, + ), + ), + ], + )), + ], + ), + ), + ); + } + + Widget _buildUniformField({ + required String label, + required IconData icon, + required Widget child, + String? errorKey, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.grey.shade200, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 8), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.grey[600], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Row( + children: [ + Icon( + icon, + size: 20, + color: _errors.containsKey(errorKey ?? '') + ? Colors.red + : Colors.blue, + ), + const SizedBox(width: 12), + Expanded(child: child), + ], + ), + ), + if (_errors.containsKey(errorKey ?? '')) + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Text( + _errors[errorKey]!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + bool _validateAllFields() { + setState(() { + _errors.clear(); + _hasErrors = false; + + final name = _nameController.text.trim(); + if (name.isEmpty) { + _errors['name'] = 'Name is required'; + _hasErrors = true; + } else if (name.length < 2 && + RegExp(r'^[A-Za-z]+([.\s]?[A-Za-z]+)*$').hasMatch(name)) { + _errors['name'] = 'Name must be at least 2 characters'; + _hasErrors = true; + } + + final phoneNumber = _phoneController.text.trim(); + if (phoneNumber.isEmpty) { + _errors['phone'] = 'Phone number is required'; + _hasErrors = true; + } else if (!RegExp(r'^\d{10}$').hasMatch(phoneNumber)) { + _errors['phone'] = 'Enter a valid 10-digit phone number'; + _hasErrors = true; + } + + if (_gender == null) { + _errors['gender'] = 'Please select a gender'; + _hasErrors = true; + } + + if (_dateOfBirth == null) { + _errors['dob'] = 'Date of Birth is required'; + _hasErrors = true; + } else { + final age = DateTime.now().difference(_dateOfBirth!).inDays ~/ 365; + if (age < 18) { + _errors['dob'] = 'User must be at least 18 years old'; + _hasErrors = true; + } + } + + if (_image == null) { + _errors['image'] = 'Profile picture is required'; + _hasErrors = true; + } + + final address = _controller.model.address; + if (address.houseNo?.isEmpty ?? true) { + _errors['address'] = 'Please complete all required address fields'; + _hasErrors = true; + } + }); + + return !_hasErrors; + } + + void _showValidationErrors() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 8), + Text('Validation Errors'), + ], + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: _errors.entries + .map((error) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + '• ${error.value}', + style: const TextStyle(color: Colors.red), + ), + )) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + Future _handleProfileSave() async { + if (_validateAllFields()) { + try { + final bool isSuccess = await _controller.savePatientData(); + if (mounted) { + Navigator.pop(context); + } + + _showResultDialog(isSuccess); + } catch (e) { + if (mounted) { + Navigator.pop(context); + } + + _showResultDialog(false); + } + } else { + _showValidationErrors(); + } + } + + Widget _buildNavigationField( + String label, IconData icon, VoidCallback onTap) { + bool isAddressField = label == 'Address'; + bool hasAddressError = _errors.containsKey('address'); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (isAddressField && hasAddressError) + ? Colors.red.withOpacity(0.1) + : Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, + color: (isAddressField && hasAddressError) + ? Colors.red + : Colors.blue, + size: 24), + ), + title: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: (isAddressField && hasAddressError) + ? Colors.red + : Colors.black, + ), + ), + subtitle: isAddressField ? _buildAddressSubtitle() : null, + trailing: Icon( + Icons.chevron_right, + color: + (isAddressField && hasAddressError) ? Colors.red : Colors.blue, + ), + onTap: onTap, + ), + if (isAddressField && hasAddressError) + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8), + child: Text( + _errors['address']!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), + ], + ); + } + + Widget _buildAddressSubtitle() { + final address = _controller.model.address; + if (address.houseNo == null || + address.line == null || + address.city == null) { + return const Text( + 'No address added', + style: TextStyle(color: Colors.grey), + ); + } + + return Text( + '${address.houseNo}, ${address.line}\n' + '${address.city}, ${address.state} ${address.pincode}\n', + style: const TextStyle(color: Colors.black87), + ); + } +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 24e86a0..ff800ec 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "telemednet") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.cosqnet.telemednet") +set(APPLICATION_ID "com.cosqnet.medoraprovider") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 1084abd..002e4b7 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -385,7 +385,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/telemednet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/telemednet"; @@ -399,7 +399,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/telemednet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/telemednet"; @@ -413,7 +413,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/telemednet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/telemednet"; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 0ca44de..719480f 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = telemednet // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.telemednet +PRODUCT_BUNDLE_IDENTIFIER = com.cosqnet.medoraprovider // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2024 com.cosqnet. All rights reserved.