doctor ratings.

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

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

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

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

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

@ -1,9 +1,6 @@
import 'package:easy_localization/easy_localization.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/extensions/string_extensions.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/theme/colors.dart';
@ -15,188 +12,289 @@ class DoctorRatingDetails extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<BookAppointmentsViewModel>(builder: (context, bookAppointmentsVM, child) {
return bookAppointmentsVM.isDoctorRatingDetailsLoading
? 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,),
],
),
if (bookAppointmentsVM.isDoctorRatingDetailsLoading) {
return Utils.getLoadingWidget();
}
RatingBar(
initialRating: bookAppointmentsVM.doctorsProfileResponseModel.actualDoctorRate!.toDouble(),
direction: Axis.horizontal,
allowHalfRating: true,
itemCount: 5,
itemSize: 20.h,
ignoreGestures: true,
ratingWidget: RatingWidget(
full: Icon(
Icons.star,
color: AppColors.ratingColorYellow,
size: 24.h,
),
half: Icon(
Icons.star_half,
color: AppColors.ratingColorYellow,
),
empty: Icon(
Icons.star,
color: AppColors.ratingColorYellow,
),
),
tapOnlyMode: true,
unratedColor: Colors.grey[500],
itemPadding: EdgeInsets.symmetric(horizontal: 4.0),
onRatingUpdate: (rating) {
print(rating);
},
),
],
),
SizedBox(height: 8.h),
Container(
margin: EdgeInsets.only(top: 10.0),
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.excellent.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))),
getRatingLine(bookAppointmentsVM.doctorDetailsList[0].ratio, Colors.green[700]!),
],
),
Container(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Text("${getRatingWidth(bookAppointmentsVM.doctorDetailsList[0].ratio).round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
),
],
),
final rating = bookAppointmentsVM.doctorsProfileResponseModel.decimalDoctorRate?.toDouble() ?? 0.0;
if (rating == 0 || rating == 0.0) {
return NoRatingDialog();
}
return DoctorRatingDialog(
averageRating: rating,
totalReviews: (bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate ?? 0).toInt(),
fiveStarPercent: bookAppointmentsVM.doctorDetailsList.isNotEmpty ? bookAppointmentsVM.doctorDetailsList[0].ratio.round() : 0,
fourStarPercent: bookAppointmentsVM.doctorDetailsList.length > 1 ? bookAppointmentsVM.doctorDetailsList[1].ratio.round() : 0,
threeStarPercent: bookAppointmentsVM.doctorDetailsList.length > 2 ? bookAppointmentsVM.doctorDetailsList[2].ratio.round() : 0,
twoStarPercent: bookAppointmentsVM.doctorDetailsList.length > 3 ? bookAppointmentsVM.doctorDetailsList[3].ratio.round() : 0,
oneStarPercent: bookAppointmentsVM.doctorDetailsList.length > 4 ? bookAppointmentsVM.doctorDetailsList[4].ratio.round() : 0,
);
});
}
}
class DoctorRatingDialog extends StatelessWidget {
final double averageRating;
final int totalReviews;
final int fiveStarPercent;
final int fourStarPercent;
final int threeStarPercent;
final int twoStarPercent;
final int oneStarPercent;
const DoctorRatingDialog({
super.key,
required this.averageRating,
required this.totalReviews,
required this.fiveStarPercent,
required this.fourStarPercent,
required this.threeStarPercent,
required this.twoStarPercent,
required this.oneStarPercent,
});
/// Returns the color for the rating badge and progress bar based on star level.
/// 5 stars -> bright green
/// 4 stars -> slightly darker green
/// 3 stars -> dark yellow / olive
/// 2 stars -> dark orange / brown-red
/// 1 star -> red
Color _getBarColor(int stars) {
switch (stars) {
case 5:
return AppColors.ratingFiveStarColor;
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: 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.vGood.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))),
getRatingLine(bookAppointmentsVM.doctorDetailsList[1].ratio, Color(0xffB7B723)),
],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
averageRating.toStringAsFixed(1),
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.w700,
color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
height: 1.1,
),
Container(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Text("${bookAppointmentsVM.doctorDetailsList[1].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
],
),
),
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)),
],
decoration: BoxDecoration(
color: averageRating == 0 || averageRating == 0.0 ? AppColors.greyF7Color : const Color.fromRGBO(0, 0, 0, 0.15),
borderRadius: BorderRadius.circular(8),
),
Container(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Text("${bookAppointmentsVM.doctorDetailsList[2].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
child: Text(
'$totalReviews Review',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
),
),
],
),
),
],
),
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.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))),
getRatingLine(bookAppointmentsVM.doctorDetailsList[3].ratio, Color(0xffEB7227)),
],
),
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")),
),
],
),
),
const SizedBox(width: 16),
// Right: Star Breakdown
Expanded(
child: Column(
children: ratingRows
.map(
(row) => _buildRatingRow(row.stars, row.percent),
)
.toList(),
),
Container(
margin: EdgeInsets.only(bottom: 30.0),
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.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))),
getRatingLine(bookAppointmentsVM.doctorDetailsList[4].ratio, Color(0xffE20C0C)),
],
),
Container(
margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
),
],
),
],
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildRatingRow(int stars, int percent) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
// Star icons
SizedBox(
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) {
var width = patientNumber;
return width.roundToDouble();
const SizedBox(width: 8),
// 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(
margin: EdgeInsets.only(top: 10.0),
child: Stack(children: [
SizedBox(
width: 150.0,
height: 7.h,
child: Container(
color: Colors.grey[300],
width: 420,
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 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(
width: patientNumber * 1.55,
height: 7.h,
child: Container(
color: color,
const SizedBox(height: 12),
// Description
Text(
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 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
static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B);
static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05);

Loading…
Cancel
Save