From c00fe3e10c0e674339d4aef3d78625c44da98eb6 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 4 Aug 2026 16:29:34 +0300 Subject: [PATCH] UI/UX updates --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 2 +- lib/core/app_state.dart | 4 +- .../book_appointments_view_model.dart | 16 +- lib/generated/locale_keys.g.dart | 1 + .../region_list_widget.dart | 9 ++ .../book_appointment/select_clinic_page.dart | 4 +- .../book_appointment/select_doctor_page.dart | 3 +- .../widgets/appointment_calendar.dart | 81 ++++++++-- lib/presentation/home/landing_page.dart | 4 +- .../home/widgets/large_service_card.dart | 8 +- .../home/widgets/welcome_widget.dart | 10 +- lib/widgets/buttons/custom_button.dart | 146 +++++++++++------- 15 files changed, 208 insertions(+), 90 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index b2bc5c2d..c025a030 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1968,5 +1968,6 @@ "notAvailable": "غير متاح", "hide": "إخفاء", "viewPatientQR": "عرض رمز الاستجابة السريعة للمريض", - "patientQR": "رمز الاستجابة السريعة للمريض" + "patientQR": "رمز الاستجابة السريعة للمريض", + "forText": "عن" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 0e3429e4..69e0d66d 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1958,7 +1958,8 @@ "notAvailable": "Not Available", "hide": "Hide", "viewPatientQR": "View Patient QR Code", - "patientQR": "Patient QR Code" + "patientQR": "Patient QR Code", + "forText": "For" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 3bfbf6cb..4829b58c 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 2122445; + body['TokenID'] = "@dm!n"; + // body['PatientID'] = 5421804; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 8f2deaa3..8f1455ad 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index e627c7aa..a27b727e 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -50,8 +50,8 @@ class AppState { bool isPayfortHostedPageEnabled = true; void setIsPaytabsEnabled(bool value) { - // isPaytabsEnabled = value; - isPaytabsEnabled = true; + isPaytabsEnabled = value; + // isPaytabsEnabled = true; } void setIsPayfortHostedPageEnabled(bool value) { diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index e4f028d9..9ed93c0a 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -420,14 +420,14 @@ class BookAppointmentsViewModel extends ChangeNotifier { setIsNearestAppointmentSelected(bool isNearestAppointmentSelected) { this.isNearestAppointmentSelected = isNearestAppointmentSelected; if (isNearestAppointmentSelected) { - for (var group in doctorsListGrouped) { - group.sort((a, b) { - var aSlot = a.nearestFreeSlot; - var bSlot = b.nearestFreeSlot; - if (aSlot == null || bSlot == null) return 0; - return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot)); - }); - } + // for (var group in doctorsListGrouped) { + // group.sort((a, b) { + // var aSlot = a.nearestFreeSlot; + // var bSlot = b.nearestFreeSlot; + // if (aSlot == null || bSlot == null) return 0; + // return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot)); + // }); + // } } else { // for (var group in doctorsListGrouped) { // group.sort((a, b) { diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 51a687a8..536f3355 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1950,5 +1950,6 @@ abstract class LocaleKeys { static const hide = 'hide'; static const viewPatientQR = 'viewPatientQR'; static const patientQR = 'patientQR'; + static const forText = 'forText'; } diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index b689162c..3a74306e 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:lottie/lottie.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -117,6 +118,14 @@ class _RegionBottomSheetBodyState extends State { height: MediaQuery.of(context).size.height * 0.5, child: Column( children: [ + // AppCustomChipWidget( + // richText: Row( + // children: [ + // "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isBold: true), + // LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor, isBold: true), + // ], + // ), + // ), // Consumer( // builder: (context, regionVM, _) { // return Padding( diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index cf1cd897..e0a810ed 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -1202,7 +1202,9 @@ class _SelectClinicPageState extends State { getTitle(AppointmentViaRegionViewmodel data) { if (data.selectedRegionId == null) { - return LocaleKeys.selectRegion.tr(context: context).toText20(isBold: true); + return Expanded( + child: "${LocaleKeys.selectRegion.tr(context: context)} ${LocaleKeys.forText.tr(context: context)} '${bookAppointmentsViewModel.selectedClinic.clinicDescription}'".toText20(isBold: true), + ); } else { return Transform.flip( flipX: data.isArabic ? true : false, diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 26ab5519..132cd46a 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -57,7 +57,8 @@ class _SelectDoctorPageState extends State { } else if (bookAppointmentsViewModel.isGetDocForHealthCal) { bookAppointmentsViewModel.getDoctorsListByHealthCal(); } else { - bookAppointmentsViewModel.getDoctorsList(); + bookAppointmentsViewModel.setIsNearestAppointmentSelected(true); + bookAppointmentsViewModel.getDoctorsList(isNearest: true); } } }); diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index ea954e03..c84e909f 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -95,20 +95,23 @@ class _AppointmentCalendarState extends State { // ), SizedBox( height: MediaQuery.of(context).size.height * 0.45, - child: Directionality( - textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr, - child: Localizations.override( - context: context, - locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'), - child: ClarityUnmask( - child: SfCalendar( - controller: _calendarController, - minDate: DateTime.now(), - showNavigationArrow: true, - // headerHeight: 60.h, - headerStyle: CalendarHeaderStyle( + child: Stack( + children: [ + Directionality( + textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr, + child: Localizations.override( + context: context, + locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'), + child: ClarityUnmask( + child: SfCalendar( + controller: _calendarController, + minDate: DateTime.now(), + initialDisplayDate: DateUtil.convertStringToDate(bookAppointmentsViewModel.freeSlotsResponse[0]), + showNavigationArrow: false, + headerHeight: 60.h, + headerStyle: CalendarHeaderStyle( backgroundColor: AppColors.transparent, - textAlign: isArabic ? TextAlign.end : TextAlign.start, + // textAlign: isArabic ? TextAlign.end : TextAlign.start, textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor), ), viewHeaderStyle: ViewHeaderStyle( @@ -153,6 +156,56 @@ class _AppointmentCalendarState extends State { ), ), ), + // Custom Navigation Arrows + Positioned( + top: 0, + right: isArabic ? null : 0, + left: isArabic ? 0 : null, + child: SizedBox( + height: 60.h, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Previous Month Arrow + GestureDetector( + onTap: () { + setState(() { + _calendarController.backward!(); + }); + }, + child: Container( + padding: EdgeInsets.all(8.h), + child: Icon( + // isArabic ? Icons.arrow_forward_ios : Icons.arrow_back_ios, + Icons.arrow_back_ios, + size: 24.h, + color: AppColors.primaryRedColor, + ), + ), + ), + // Next Month Arrow + GestureDetector( + onTap: () { + setState(() { + _calendarController.forward!(); + }); + }, + child: Container( + padding: EdgeInsets.all(8.h), + child: Icon( + // isArabic ? Icons.arrow_back_ios : Icons.arrow_forward_ios, + Icons.arrow_forward_ios, + size: 24.h, + color: AppColors.primaryRedColor, + ), + ), + ), + ], + ), + ), + ), + ], + ), ), SizedBox(height: 10.h), Transform.translate( @@ -164,7 +217,7 @@ class _AppointmentCalendarState extends State { ? ClarityUnmask( child: ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.15, + maxHeight: MediaQuery.of(context).size.height * 0.25, minHeight: 0, ), child: Directionality( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 85f1e972..b3ba5e78 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -253,8 +253,8 @@ class _LandingPageState extends State { padding: EdgeInsets.only( top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) ? (MediaQuery.paddingOf(context).top + 70.h) - : kToolbarHeight + 0.h, - bottom: 24), + : kToolbarHeight + 16.h, + bottom: 24.h), child: Column( spacing: 16.h, children: [ diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index 7c037604..a0d9553d 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -128,7 +128,9 @@ class LargeServiceCard extends StatelessWidget { ), ], ), - ); + ).onPress(() { + handleOnTap(); + }); } void handleOnTap() { @@ -275,7 +277,9 @@ class FadedLargeServiceCard extends StatelessWidget { ), ], ), - ); + ).onPress(() { + handleOnTap(); + }); } void handleOnTap() { diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 039da8c1..e3d96e3f 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -41,9 +41,13 @@ class WelcomeWidget extends StatelessWidget { borderRadius: BorderRadius.circular(30), child: Row( mainAxisSize: MainAxisSize.min, - spacing: 8.h, + // spacing: 8.h, children: [ - Icon(Icons.menu, color: AppColors.textColor).onPress(() { + SizedBox( + height: 40.h, + width: 20.w, + child: Icon(Icons.menu, color: AppColors.textColor), + ).onPress(() { getIt.get().getProfileSettings(); Navigator.of(context).push( CustomPageRoute( @@ -92,7 +96,7 @@ class WelcomeWidget extends StatelessWidget { ], ), ], - ).expanded, + ).paddingOnly(left: 8.h, right: 8.h).expanded, ], ), ), diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index f3eb94fb..82b62507 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -6,7 +6,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -class CustomButton extends StatelessWidget { +class CustomButton extends StatefulWidget { final String text; final String? icon; final VoidCallback? onPressed; @@ -54,25 +54,61 @@ class CustomButton extends StatelessWidget { this.applyThemeColor = true, }); + @override + State createState() => _CustomButtonState(); +} + +class _CustomButtonState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 80), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _playTapAnimation() async { + if (!widget.isDisabled && widget.onPressed != null) { + await _animationController.forward(); + _animationController.reverse(); + } + } + @override Widget build(BuildContext context) { - final radius = borderRadius ?? (12.r); - final iconS = iconSize ?? (24.h); - final fontS = fontSize ?? (16.f); - final isButtonDisabled = isDisabled || onPressed == null; + final radius = widget.borderRadius ?? (12.r); + final iconS = widget.iconSize ?? (24.h); + final fontS = widget.fontSize ?? (16.f); + final isButtonDisabled = widget.isDisabled || widget.onPressed == null; return Semantics( button: true, enabled: !isButtonDisabled, - label: text, - hint: isButtonDisabled ? 'Button is disabled' : 'Tap to $text', - child: GestureDetector( + label: widget.text, + hint: isButtonDisabled ? 'Button is disabled' : 'Tap to ${widget.text}', + child: GestureDetector( onTap: isButtonDisabled - ? null - : () { + ? null + : () async { + // Play tap animation and wait for it to complete + await _playTapAnimation(); + // Log button tap to Clarity before executing callback try { - Clarity.setCustomTag('button_text', text.length > 100 ? text.substring(0, 100) : text); + Clarity.setCustomTag('button_text', widget.text.length > 100 ? widget.text.substring(0, 100) : widget.text); Clarity.setCustomTag('button_action', 'tap'); Clarity.setCustomTag('button_disabled', 'false'); Clarity.sendCustomEvent('button_tapped'); @@ -81,67 +117,73 @@ class CustomButton extends StatelessWidget { } // Execute the actual button callback - onPressed?.call(); + widget.onPressed?.call(); }, - child: Container( - height: height == null - ? 56.h + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) => Transform.scale( + scale: _scaleAnimation.value, + child: child, + ), + child: Container( + height: widget.height == null + ? 56.h : (isFoldable || isTablet) - ? height! + 10.h - : height, - width: width, - padding: padding, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor, - borderRadius: radius, + ? widget.height! + 10.h + : widget.height, + width: widget.width, + padding: widget.padding, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : widget.backgroundColor, + borderRadius: radius, customBorder: BorderRadius.circular(radius), - side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)), - child: Row( + side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : widget.borderColor)), + child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (icon != null) - Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, - child: Utils.buildSvgWithAssets( - icon: icon!, - iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor, - isDisabled: isButtonDisabled, + if (widget.icon != null) + Padding( + padding: widget.text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, + child: Utils.buildSvgWithAssets( + icon: widget.icon!, + iconColor: isButtonDisabled ? AppColors.greyTextColor : widget.iconColor, + isDisabled: isButtonDisabled, width: iconS, height: iconS, - applyThemeColor: applyThemeColor), - ), - if (text.isNotEmpty && icon != null) - Flexible( + applyThemeColor: widget.applyThemeColor), + ), + if (widget.text.isNotEmpty && widget.icon != null) + Flexible( child: Text( - text, - overflow: textOverflow ?? TextOverflow.ellipsis, - maxLines: 1, + widget.text, + overflow: widget.textOverflow ?? TextOverflow.ellipsis, + maxLines: 1, textAlign: TextAlign.center, style: context.dynamicTextStyle( fontSize: fontS, - color: isButtonDisabled ? AppColors.greyTextColor : textColor, - letterSpacing: 0, - fontWeight: isBold ? FontWeight.w700 : fontWeight, - ), + color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor, + letterSpacing: 0, + fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight, + ), ), ), - if (text.isNotEmpty && icon == null) - Text( - text, - overflow: textOverflow ?? TextOverflow.ellipsis, - maxLines: 1, + if (widget.text.isNotEmpty && widget.icon == null) + Text( + widget.text, + overflow: widget.textOverflow ?? TextOverflow.ellipsis, + maxLines: 1, textAlign: TextAlign.center, style: context.dynamicTextStyle( fontSize: fontS, - color: isButtonDisabled ? AppColors.greyTextColor : textColor, - letterSpacing: 0, - fontWeight: isBold ? FontWeight.w700 : fontWeight, - ), + color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor, + letterSpacing: 0, + fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight, + ), ), ], ), )), - ); + )); } }