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,7 +34,8 @@ 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
? Consumer<BookAppointmentsViewModel>(
builder: (context, viewModel, child) { builder: (context, viewModel, child) {
return SizedBox( return SizedBox(
width: 24.h, width: 24.h,
@ -67,7 +68,8 @@ class DoctorProfilePage extends StatelessWidget {
}), }),
); );
}, },
) : 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( }
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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
bookAppointmentsVM.doctorsProfileResponseModel.decimalDoctorRate!.toString().toText44(isBold: true, isEnglishOnly: true),
SizedBox(height: 4.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Container(
width: 130,
height: 160,
decoration: BoxDecoration(
color: _getBadgeColor(averageRating),
borderRadius: BorderRadius.circular(18),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
"${bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate} " Text(
.toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor, isEnglishOnly: true), averageRating.toStringAsFixed(1),
LocaleKeys.reviews.tr(context: context) style: TextStyle(
.toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor,), fontSize: 60,
], fontWeight: FontWeight.w700,
color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
height: 1.1,
), ),
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( const SizedBox(height: 8),
Icons.star_half, Container(
color: AppColors.ratingColorYellow, padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
), ),
empty: Icon( decoration: BoxDecoration(
Icons.star, color: averageRating == 0 || averageRating == 0.0 ? AppColors.greyF7Color : const Color.fromRGBO(0, 0, 0, 0.15),
color: AppColors.ratingColorYellow, borderRadius: BorderRadius.circular(8),
), ),
child: Text(
'$totalReviews Review',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor,
), ),
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")),
), ),
],
const SizedBox(width: 16),
// Right: Star Breakdown
Expanded(
child: Column(
children: ratingRows
.map(
(row) => _buildRatingRow(row.stars, row.percent),
)
.toList(),
), ),
), ),
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)),
], ],
), ),
Container( const SizedBox(height: 16),
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")),
),
], ],
), ),
), );
Container( }
Widget _buildRatingRow(int stars, int percent) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( // Star icons
children: [ SizedBox(
Container( width: 70,
width: 100.0, child: Row(
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), mainAxisAlignment: MainAxisAlignment.start,
child: Text(LocaleKeys.good.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), children: List.generate(
getRatingLine(bookAppointmentsVM.doctorDetailsList[2].ratio, Color(0xffEBA727)), stars,
], (_) => Icon(Icons.star, size: 14, color: AppColors.ratingStarIconColor),
),
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")),
), ),
],
), ),
), ),
Container(
child: Row( const SizedBox(width: 8),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Expanded(
children: [ child: ClipRRect(
Row( borderRadius: BorderRadius.circular(6),
children: [ child: SizedBox(
Container( height: 10,
width: 100.0, child: LinearProgressIndicator(
margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), value: percent / 100,
child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), backgroundColor: AppColors.shimmerBaseColor,
getRatingLine(bookAppointmentsVM.doctorDetailsList[3].ratio, Color(0xffEB7227)), valueColor: AlwaysStoppedAnimation<Color>(
], _getBarColor(stars),
), ),
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,
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), const SizedBox(width: 8),
child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")),
// Percentage text
SizedBox(
width: 36,
child: Text(
'$percent%',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.ratingPercentageTextColor,
), ),
],
), ),
), ),
], ],
),
); );
});
} }
}
double getRatingWidth(num patientNumber) { class _RatingRowData {
var width = patientNumber; final int stars;
return width.roundToDouble(); final int percent;
}
Widget getRatingLine(double patientNumber, Color color) { _RatingRowData({required this.stars, required this.percent});
}
class NoRatingDialog extends StatelessWidget {
const NoRatingDialog({super.key});
@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,
), ),
SizedBox(
width: patientNumber * 1.55,
height: 7.h,
child: Container(
color: color,
), ),
const SizedBox(height: 20),
// Title
Text(
LocaleKeys.noRatingAvailable.tr(context: context),
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppColors.textColor,
),
textAlign: TextAlign.center,
),
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 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