doctor ratings.

faiz_dev
aamir-csol 2 days ago
parent 8112f9bb0a
commit 5be4f66718

@ -3,6 +3,8 @@
"arabic": "العربية", "arabic": "العربية",
"login": "تسجيل الدخول", "login": "تسجيل الدخول",
"noDataAvailable": "لا توجد بيانات متاحة", "noDataAvailable": "لا توجد بيانات متاحة",
"noRatingAvailable": "لا يوجد تقييم متاح",
"doctorDoesNotHaveRating": "لم يحصل هذا الطبيب على أي تقييمات بعد.",
"confirm": "تأكيد", "confirm": "تأكيد",
"loadingText": "جاري التحميل، يرجى الانتظار...", "loadingText": "جاري التحميل، يرجى الانتظار...",
"kilometerUnit": "كم", "kilometerUnit": "كم",

@ -3,6 +3,8 @@
"arabic": "Arabic", "arabic": "Arabic",
"login": "Login", "login": "Login",
"noDataAvailable": "No Data Available", "noDataAvailable": "No Data Available",
"noRatingAvailable": "No Rating Available",
"doctorDoesNotHaveRating": "This doctor does not have any ratings yet.",
"confirm": "Confirm", "confirm": "Confirm",
"loadingText": "Loading, please wait...", "loadingText": "Loading, please wait...",
"kilometerUnit": "KM", "kilometerUnit": "KM",

@ -7,6 +7,8 @@ abstract class LocaleKeys {
static const arabic = 'arabic'; static const arabic = 'arabic';
static const login = 'login'; static const login = 'login';
static const noDataAvailable = 'noDataAvailable'; static const noDataAvailable = 'noDataAvailable';
static const noRatingAvailable = 'noRatingAvailable';
static const doctorDoesNotHaveRating = 'doctorDoesNotHaveRating';
static const confirm = 'confirm'; static const confirm = 'confirm';
static const loadingText = 'loadingText'; static const loadingText = 'loadingText';
static const kilometerUnit = 'kilometerUnit'; static const kilometerUnit = 'kilometerUnit';

@ -34,40 +34,42 @@ class DoctorProfilePage extends StatelessWidget {
Expanded( Expanded(
child: CollapsingListView( child: CollapsingListView(
title: LocaleKeys.profileOnlyText.tr(), title: LocaleKeys.profileOnlyText.tr(),
trailing: appState.isAuthenticated ? Consumer<BookAppointmentsViewModel>( trailing: appState.isAuthenticated
builder: (context, viewModel, child) { ? Consumer<BookAppointmentsViewModel>(
return SizedBox( builder: (context, viewModel, child) {
width: 24.h, return SizedBox(
height: 24.h, width: 24.h,
child: Icon( height: 24.h,
viewModel.isFavouriteDoctor ? Icons.bookmark : Icons.bookmark_border_outlined, child: Icon(
size: 36.h, viewModel.isFavouriteDoctor ? Icons.bookmark : Icons.bookmark_border_outlined,
color: viewModel.isFavouriteDoctor ? AppColors.primaryRedColor : AppColors.textColor, size: 36.h,
).onPress(() async { color: viewModel.isFavouriteDoctor ? AppColors.primaryRedColor : AppColors.textColor,
viewModel.toggleFavouriteDoctor(); ).onPress(() async {
await viewModel.insertFavouriteDoctor( viewModel.toggleFavouriteDoctor();
patientID: appState.getAuthenticatedUser()!.patientId!, await viewModel.insertFavouriteDoctor(
projectID: viewModel.doctorsProfileResponseModel.projectID ?? 0, patientID: appState.getAuthenticatedUser()!.patientId!,
clinicID: viewModel.doctorsProfileResponseModel.clinicID ?? 0, projectID: viewModel.doctorsProfileResponseModel.projectID ?? 0,
doctorID: viewModel.doctorsProfileResponseModel.doctorID ?? 0, clinicID: viewModel.doctorsProfileResponseModel.clinicID ?? 0,
isActive: viewModel.isFavouriteDoctor, doctorID: viewModel.doctorsProfileResponseModel.doctorID ?? 0,
onSuccess: (response) { isActive: viewModel.isFavouriteDoctor,
// Successfully added/removed favorite - refresh the favorites list onSuccess: (response) {
getIt.get<MyAppointmentsViewModel>().refreshFavouriteDoctors(); // Successfully added/removed favorite - refresh the favorites list
print( getIt.get<MyAppointmentsViewModel>().refreshFavouriteDoctors();
viewModel.isFavouriteDoctor ? "Doctor added to favorites" : "Doctor removed from favorites", print(
); viewModel.isFavouriteDoctor ? "Doctor added to favorites" : "Doctor removed from favorites",
}, );
onError: (error) { },
// Revert the state on error onError: (error) {
viewModel.toggleFavouriteDoctor(); // Revert the state on error
Utils.showToast(error); viewModel.toggleFavouriteDoctor();
}, Utils.showToast(error);
); },
}), );
); }),
}, );
) : SizedBox.shrink(), },
)
: SizedBox.shrink(),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -204,7 +206,6 @@ class DoctorProfilePage extends StatelessWidget {
bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription; bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription;
bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName; bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName;
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
bookAppointmentsViewModel.isLiveCareSchedule bookAppointmentsViewModel.isLiveCareSchedule
? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots(

@ -1,9 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
@ -15,188 +12,289 @@ class DoctorRatingDetails extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) { return Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return bookAppointmentsVM.isDoctorRatingDetailsLoading if (bookAppointmentsVM.isDoctorRatingDetailsLoading) {
? Utils.getLoadingWidget() return Utils.getLoadingWidget();
: Column( }
crossAxisAlignment: CrossAxisAlignment.start,
children: [
bookAppointmentsVM.doctorsProfileResponseModel.decimalDoctorRate!.toString().toText44(isBold: true, isEnglishOnly: true),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
"${bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate} "
.toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor, isEnglishOnly: true),
LocaleKeys.reviews.tr(context: context)
.toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor,),
],
),
RatingBar( final rating = bookAppointmentsVM.doctorsProfileResponseModel.decimalDoctorRate?.toDouble() ?? 0.0;
initialRating: bookAppointmentsVM.doctorsProfileResponseModel.actualDoctorRate!.toDouble(), if (rating == 0 || rating == 0.0) {
direction: Axis.horizontal, return NoRatingDialog();
allowHalfRating: true, }
itemCount: 5,
itemSize: 20.h, return DoctorRatingDialog(
ignoreGestures: true, averageRating: rating,
ratingWidget: RatingWidget( totalReviews: (bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate ?? 0).toInt(),
full: Icon( fiveStarPercent: bookAppointmentsVM.doctorDetailsList.isNotEmpty ? bookAppointmentsVM.doctorDetailsList[0].ratio.round() : 0,
Icons.star, fourStarPercent: bookAppointmentsVM.doctorDetailsList.length > 1 ? bookAppointmentsVM.doctorDetailsList[1].ratio.round() : 0,
color: AppColors.ratingColorYellow, threeStarPercent: bookAppointmentsVM.doctorDetailsList.length > 2 ? bookAppointmentsVM.doctorDetailsList[2].ratio.round() : 0,
size: 24.h, twoStarPercent: bookAppointmentsVM.doctorDetailsList.length > 3 ? bookAppointmentsVM.doctorDetailsList[3].ratio.round() : 0,
), oneStarPercent: bookAppointmentsVM.doctorDetailsList.length > 4 ? bookAppointmentsVM.doctorDetailsList[4].ratio.round() : 0,
half: Icon( );
Icons.star_half, });
color: AppColors.ratingColorYellow, }
), }
empty: Icon(
Icons.star, class DoctorRatingDialog extends StatelessWidget {
color: AppColors.ratingColorYellow, final double averageRating;
), final int totalReviews;
), final int fiveStarPercent;
tapOnlyMode: true, final int fourStarPercent;
unratedColor: Colors.grey[500], final int threeStarPercent;
itemPadding: EdgeInsets.symmetric(horizontal: 4.0), final int twoStarPercent;
onRatingUpdate: (rating) { final int oneStarPercent;
print(rating);
}, const DoctorRatingDialog({
), super.key,
], required this.averageRating,
), required this.totalReviews,
SizedBox(height: 8.h), required this.fiveStarPercent,
Container( required this.fourStarPercent,
margin: EdgeInsets.only(top: 10.0), required this.threeStarPercent,
child: Row( required this.twoStarPercent,
mainAxisAlignment: MainAxisAlignment.spaceBetween, required this.oneStarPercent,
children: [ });
Row(
children: [ /// Returns the color for the rating badge and progress bar based on star level.
Container( /// 5 stars -> bright green
width: 100.0, /// 4 stars -> slightly darker green
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), /// 3 stars -> dark yellow / olive
child: Text(LocaleKeys.excellent.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), /// 2 stars -> dark orange / brown-red
getRatingLine(bookAppointmentsVM.doctorDetailsList[0].ratio, Colors.green[700]!), /// 1 star -> red
], Color _getBarColor(int stars) {
), switch (stars) {
Container( case 5:
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), return AppColors.ratingFiveStarColor;
child: Text("${getRatingWidth(bookAppointmentsVM.doctorDetailsList[0].ratio).round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), case 4:
), return AppColors.ratingFourStarColor;
], case 3:
), return AppColors.ratingThreeStarColor;
case 2:
return AppColors.ratingTwoStarColor;
case 1:
return AppColors.ratingOneStarColor;
default:
return Colors.grey;
}
}
/// Returns the background color for the large rating badge based on the
/// overall average rating.
Color _getBadgeColor(double rating) {
if (rating == 0 || rating == 0.0) {
return AppColors.greyLightColor; // Grey color for no rating
} else if (rating >= 4.5) {
return AppColors.ratingFiveStarColor; // Bright green
} else if (rating >= 4.0) {
return AppColors.ratingBadgeFiveStarColor; // Green
} else if (rating >= 3.0) {
return AppColors.ratingThreeStarColor;
} else if (rating >= 2.0) {
return AppColors.ratingTwoStarColor;
} else {
return AppColors.ratingOneStarColor;
}
}
@override
Widget build(BuildContext context) {
final ratingRows = [
_RatingRowData(stars: 5, percent: fiveStarPercent),
_RatingRowData(stars: 4, percent: fourStarPercent),
_RatingRowData(stars: 3, percent: threeStarPercent),
_RatingRowData(stars: 2, percent: twoStarPercent),
_RatingRowData(stars: 1, percent: oneStarPercent),
];
return Container(
width: 420,
padding: const EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 130,
height: 160,
decoration: BoxDecoration(
color: _getBadgeColor(averageRating),
borderRadius: BorderRadius.circular(18),
), ),
Container( child: Column(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Text(
Row( averageRating.toStringAsFixed(1),
children: [ style: TextStyle(
Container( fontSize: 60,
width: 100.0, fontWeight: FontWeight.w700,
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
child: Text(LocaleKeys.vGood.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), height: 1.1,
getRatingLine(bookAppointmentsVM.doctorDetailsList[1].ratio, Color(0xffB7B723)),
],
), ),
Container( ),
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), const SizedBox(height: 8),
child: Text("${bookAppointmentsVM.doctorDetailsList[1].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
), ),
], decoration: BoxDecoration(
), color: averageRating == 0 || averageRating == 0.0 ? AppColors.greyF7Color : const Color.fromRGBO(0, 0, 0, 0.15),
), borderRadius: BorderRadius.circular(8),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 100.0,
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0),
child: Text(LocaleKeys.good.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))),
getRatingLine(bookAppointmentsVM.doctorDetailsList[2].ratio, Color(0xffEBA727)),
],
), ),
Container( child: Text(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), '$totalReviews Review',
child: Text("${bookAppointmentsVM.doctorDetailsList[2].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
),
), ),
], ),
), ],
), ),
Container( ),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, const SizedBox(width: 16),
children: [
Row( // Right: Star Breakdown
children: [ Expanded(
Container( child: Column(
width: 100.0, children: ratingRows
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), .map(
child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), (row) => _buildRatingRow(row.stars, row.percent),
getRatingLine(bookAppointmentsVM.doctorDetailsList[3].ratio, Color(0xffEB7227)), )
], .toList(),
),
Container(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Text("${bookAppointmentsVM.doctorDetailsList[3].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
),
],
),
), ),
Container( ),
margin: EdgeInsets.only(bottom: 30.0), ],
child: Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, const SizedBox(height: 16),
children: [ ],
Row( ),
children: [ );
Container( }
width: 100.0,
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), Widget _buildRatingRow(int stars, int percent) {
child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), return Padding(
getRatingLine(bookAppointmentsVM.doctorDetailsList[4].ratio, Color(0xffE20C0C)), padding: const EdgeInsets.symmetric(vertical: 5),
], child: Row(
), children: [
Container( // Star icons
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), SizedBox(
child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), width: 70,
), child: Row(
], mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(
stars,
(_) => Icon(Icons.star, size: 14, color: AppColors.ratingStarIconColor),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
height: 10,
child: LinearProgressIndicator(
value: percent / 100,
backgroundColor: AppColors.shimmerBaseColor,
valueColor: AlwaysStoppedAnimation<Color>(
_getBarColor(stars),
), ),
), ),
], ),
); ),
}); ),
}
double getRatingWidth(num patientNumber) { const SizedBox(width: 8),
var width = patientNumber;
return width.roundToDouble(); // Percentage text
SizedBox(
width: 36,
child: Text(
'$percent%',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.ratingPercentageTextColor,
),
),
),
],
),
);
} }
}
class _RatingRowData {
final int stars;
final int percent;
_RatingRowData({required this.stars, required this.percent});
}
class NoRatingDialog extends StatelessWidget {
const NoRatingDialog({super.key});
Widget getRatingLine(double patientNumber, Color color) { @override
Widget build(BuildContext context) {
return Container( return Container(
margin: EdgeInsets.only(top: 10.0), width: 420,
child: Stack(children: [ padding: const EdgeInsets.all(24),
SizedBox( child: Column(
width: 150.0, mainAxisSize: MainAxisSize.min,
height: 7.h, crossAxisAlignment: CrossAxisAlignment.center,
child: Container( children: [
color: Colors.grey[300], // Icon
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.lightRedButtonColor,
borderRadius: BorderRadius.circular(24),
),
child: Icon(
Icons.star_border_rounded,
size: 48,
color: AppColors.primaryRedColor,
),
),
const SizedBox(height: 20),
// Title
Text(
LocaleKeys.noRatingAvailable.tr(context: context),
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppColors.textColor,
),
textAlign: TextAlign.center,
), ),
),
SizedBox( const SizedBox(height: 12),
width: patientNumber * 1.55,
height: 7.h, // Description
child: Container( Text(
color: color, LocaleKeys.doctorDoesNotHaveRating.tr(context: context),
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: AppColors.textColorLight,
),
textAlign: TextAlign.center,
), ),
),
]), const SizedBox(height: 8),
],
),
); );
} }
} }

@ -41,6 +41,17 @@ class AppColors {
static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15);
static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A);
// Doctor Rating Colors
static const Color ratingStarIconColor = Color(0xFFFFA726); // Orange-amber for star icons
static const Color ratingFiveStarColor = Color(0xFF4CAF50); // Green for 5 stars
static const Color ratingFourStarColor = Color(0xFF8BC34A); // Light green for 4 stars
static const Color ratingThreeStarColor = Color(0xFFC0A000); // Dark yellow/olive for 3 stars
static const Color ratingTwoStarColor = Color(0xFFD84315); // Dark orange-red for 2 stars
static const Color ratingOneStarColor = Color(0xFFD32F2F); // Red for 1 star
static const Color ratingBadgeFiveStarColor = Color(0xFF5BBD8E); // Green badge for 4.0
static const Color ratingPercentageTextColor = Color(0xFF4A4A4A); // Dark grey for percentage text
// Semantic / Status Colors // Semantic / Status Colors
static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B);
static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05); static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05);

Loading…
Cancel
Save