diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index bd66e84a..5455188f 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -895,6 +895,7 @@ "monitor": "مراقبة", "noSpecialResult": "لا توجد نتائج خاصة", "setTheDateRange": "تعيين النطاق الزمني", + "selectDate": "اختر التاريخ", "historyFlowchart": "مخطط تدفق التاريخ", "to": "إلى", "startDate": "تاريخ البدء", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index ba780f18..b7d7597c 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -878,6 +878,7 @@ "monitor": "Monitor", "noSpecialResult": "No Special Results", "setTheDateRange": "Set The Date Range", + "selectDate": "Select Date", "historyFlowchart": "History FlowChart", "to": "to", "startDate" : "Start Date", diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index f451f0d5..3b9eefc0 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -76,6 +76,7 @@ import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; @@ -133,7 +134,6 @@ class AppDependencies { navigationService: getIt(), )); - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); getIt.registerLazySingleton(() => NotificationServiceImp( flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin, @@ -172,8 +172,12 @@ class AppDependencies { getIt.registerLazySingleton(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); - getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), - ),); + getIt.registerFactory( + () => TermsConditionsViewModel( + termsConditionsRepo: getIt(), + errorHandlerService: getIt(), + ), + ); getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => NotificationsRepoImp(loggerService: getIt(), apiClient: getIt())); @@ -233,10 +237,10 @@ class AppDependencies { ); getIt.registerLazySingleton(() => ProfileSettingsViewModel( - cacheService: getIt(), - profileSettingsRepo: getIt(), - errorHandlerService: getIt(), - )); + cacheService: getIt(), + profileSettingsRepo: getIt(), + errorHandlerService: getIt(), + )); getIt.registerLazySingleton(() => DateRangeSelectorRangeViewModel()); @@ -312,22 +316,12 @@ class AppDependencies { getIt.registerLazySingleton(() => NotificationsViewModel(notificationsRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => ActivePrescriptionsViewModel( - errorHandlerService: getIt(), - activePrescriptionsRepo: getIt() - ), - ); - getIt.registerFactory( - () => QrParkingViewModel( - qrParkingRepo: getIt(), - errorHandlerService: getIt(), - cacheService: getIt(), - ), - ); + getIt.registerLazySingleton(() => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt())); + getIt.registerFactory(() => QrParkingViewModel(qrParkingRepo: getIt(), errorHandlerService: getIt(), cacheService: getIt())); getIt.registerLazySingleton(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); } } diff --git a/lib/core/utils/request_utils.dart b/lib/core/utils/request_utils.dart index bb947398..4d4ae4b5 100644 --- a/lib/core/utils/request_utils.dart +++ b/lib/core/utils/request_utils.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; -import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -12,6 +11,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/request_model import 'package:hmg_patient_app_new/features/authentication/models/request_models/send_activation_request_model.dart'; import 'package:hmg_patient_app_new/features/common/models/commong_authanticated_req_model.dart'; import 'package:hmg_patient_app_new/features/common/models/family_file_request.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; class RequestUtils { static dynamic getPatientAuthenticationRequest({ diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 573887e6..ca31b663 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:get_it/get_it.dart'; -import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.dart'; @@ -42,6 +41,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; @@ -152,10 +152,12 @@ class AuthenticationViewModel extends ChangeNotifier { void onDobChange(String? date) { if (calenderType == CalenderEnum.hijri) { - var hijriDate = HijriGregConverter.gregorianToHijri(DateTime.parse(date!)); + // Use DateRangCalenderModel for accurate Umm al-Qura conversion + final calendarModel = DateRangCalenderModel(appState: _appState, navigationService: _errorHandlerService, dialogService: _dialogService); + var hijriDate = calendarModel.gregorianToHijri(DateTime.parse(date!)); DateTime hijriDateTimeForController = DateTime(hijriDate.year, hijriDate.month, hijriDate.day); dob = Utils.formatDateForApi(date); - dobController.text = Utils.formatHijriDateToDisplay(hijriDateTimeForController.toIso8601String()); // Or directly hijriDate.toString() if that's what your formatter expects + dobController.text = Utils.formatHijriDateToDisplay(hijriDateTimeForController.toIso8601String()); } else { dobController.text = Utils.formatDateToDisplay(date!); dob = Utils.formatDateForApi(date); @@ -897,7 +899,13 @@ class AuthenticationViewModel extends ChangeNotifier { } final resultEither = await _authenticationRepo.checkPatientForRegistration(commonAuthanticatedRequest: nRequest); - resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { + resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure, onUnHandledFailure: (failur) { + + LoaderBottomSheet.hideLoader(); + _dialogService.showExceptionBottomSheet(message: failur.message, onOkPressed: () { + _navigationService.pop(); + }); + }), (apiResponse) async { checkUserStatusForRegistration(response: apiResponse.data, request: request); }); } @@ -907,6 +915,7 @@ class AuthenticationViewModel extends ChangeNotifier { // LoadingUtils.showFullScreenLoader(loadingText: "Setting up your medical file.\nMay take a moment."); var request = RequestUtils.getUserSignupCompletionRequest(fullName: nameController.text, emailAddress: emailController.text, gender: genderType, maritalStatus: maritalStatus); + final resultEither = await _authenticationRepo.registerUser(registrationPayloadDataModelRequest: request); resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { if (apiResponse.data is String) { @@ -926,6 +935,8 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pushAndRemoveUntil(CustomPageRoute(page: LandingNavigation()), (r) => false); _navigationService.push(CustomPageRoute(page: LoginScreen())); }); + } else { + print("========="); } } }); diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 486cb4da..03e2a341 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -907,6 +907,7 @@ class CodegenLoader extends AssetLoader{ "monitor": "مراقبة", "noSpecialResult": "لا توجد نتائج خاصة", "setTheDateRange": "تعيين النطاق الزمني", + "selectDate": "اختر التاريخ", "historyFlowchart": "مخطط تدفق التاريخ", "to": "إلى", "startDate": "تاريخ البدء", @@ -2627,6 +2628,7 @@ static const Map _en_US = { "monitor": "Monitor", "noSpecialResult": "No Special Results", "setTheDateRange": "Set The Date Range", + "selectDate": "Select Date", "historyFlowchart": "History FlowChart", "to": "to", "startDate": "Start Date", diff --git a/lib/generated/locale_kesy.g.dart b/lib/generated/locale_kesy.g.dart index 06f92632..3bf6d2d9 100644 --- a/lib/generated/locale_kesy.g.dart +++ b/lib/generated/locale_kesy.g.dart @@ -893,6 +893,7 @@ abstract class LocaleKeys { static const monitor = 'monitor'; static const noSpecialResult = 'noSpecialResult'; static const setTheDateRange = 'setTheDateRange'; + static const selectDate = 'selectDate'; static const historyFlowchart = 'historyFlowchart'; static const to = 'to'; static const startDate = 'startDate'; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index f6334bb4..d09e4b6b 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -894,6 +894,7 @@ abstract class LocaleKeys { static const monitor = 'monitor'; static const noSpecialResult = 'noSpecialResult'; static const setTheDateRange = 'setTheDateRange'; + static const selectDate = 'selectDate'; static const historyFlowchart = 'historyFlowchart'; static const to = 'to'; static const startDate = 'startDate'; diff --git a/lib/main.dart b/lib/main.dart index 6f086c34..23f7c4d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,6 +53,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/app_theme.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; @@ -246,6 +247,8 @@ void main() async { ) , ChangeNotifierProvider( create: (_) => getIt.get(), + ), ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), // Wrap your app diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index f01dd036..4a628fd3 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -73,7 +73,7 @@ class _RegisterNew extends State { onLanguageChanged: (lang) {}, hideLogoAndLang: true, ), - bottomSheet: Container( + bottomSheet: Container( // height: 200.h, width: MediaQuery.of(context).size.width, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -81,51 +81,52 @@ class _RegisterNew extends State { customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), hasShadow: true, ), - child: Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.cancel.tr(), - icon: AppAssets.cancel, - onPressed: () { - Navigator.of(context).pop(); - // authVM!.clearDefaultInputValues(); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - iconColor: AppColors.primaryRedColor, + child: Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + icon: AppAssets.cancel, + onPressed: () { + Navigator.of(context).pop(); + // authVM!.clearDefaultInputValues(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + iconColor: AppColors.primaryRedColor, + ), ), - ), - SizedBox(width: 16.w), - Expanded( - child: CustomButton( - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - text: LocaleKeys.confirm.tr(), - icon: AppAssets.confirm, - iconColor: AppColors.whiteColor, - onPressed: () { - if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) { - if (ValidationUtils.validateUaeRegistration( - name: authVM!.nameController.text, - gender: authVM!.genderType, - country: authVM!.pickedCountryByUAEUser, - maritalStatus: authVM!.maritalStatus, - onOkPress: () { - Navigator.of(context).pop(); - })) { + SizedBox(width: 16.w), + Expanded( + child: CustomButton( + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + text: LocaleKeys.confirm.tr(), + icon: AppAssets.confirm, + iconColor: AppColors.whiteColor, + onPressed: () { + if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) { + if (ValidationUtils.validateUaeRegistration( + name: authVM!.nameController.text, + gender: authVM!.genderType, + country: authVM!.pickedCountryByUAEUser, + maritalStatus: authVM!.maritalStatus, + onOkPress: () { + Navigator.of(context).pop(); + })) { + showModel(context: context); + } + } else { showModel(context: context); } - } else { - showModel(context: context); - } - }, - ), - ) - ], - ).paddingAll(16.h),), + }, + ), + ) + ], + ).paddingAll(16.h), + ), body: GestureDetector( onTap: () { FocusScope.of(context).unfocus(); diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index fd37d0cb..2bc5788b 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.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/extensions/widget_extensions.dart'; @@ -12,31 +13,66 @@ 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/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/models/calendar_design_type.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; typedef OnRangeSelected = void Function(DateTime? start, DateTime? end); +typedef OnCalendarTypeChanged = void Function(bool isGregorian); class DateRangeSelector extends StatefulWidget { final OnRangeSelected onRangeSelected; + final CalendarDesignType designType; + final bool showCalendarToggle; + final OnCalendarTypeChanged? onCalendarTypeChanged; - const DateRangeSelector({super.key, required this.onRangeSelected}); + const DateRangeSelector({ + super.key, + required this.onRangeSelected, + this.designType = CalendarDesignType.defaultUI, + this.showCalendarToggle = true, + this.onCalendarTypeChanged, + }); @override State createState() => _DateRangeSelectorState(); } +enum PickerViewMode { + date, + month, + year, +} + class _DateRangeSelectorState extends State { late DateRangePickerController _calendarController; DateTime? start; DateTime? end; late DateRangeSelectorRangeViewModel model; + PickerViewMode _viewMode = PickerViewMode.date; + + // Cache for Hijri conversions to avoid repeated calculations + final Map _hijriCache = {}; + + // Track the current Hijri month/year being displayed (for header display accuracy) + HijriGregDate? _currentHijriDisplay; + @override void initState() { _calendarController = DateRangePickerController(); scheduleMicrotask(() { - _calendarController.selectedRange = PickerDateRange(model.fromDate,model.toDate); + if (widget.designType == CalendarDesignType.designV2) { + // For V2, select today's date by default + final today = DateTime.now(); + _calendarController.selectedDate = today; + model.updateSelectedDate(today); + } else { + // For default UI, use range selection + _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); + } }); super.initState(); } @@ -45,9 +81,12 @@ class _DateRangeSelectorState extends State { Widget build(BuildContext context) { model = Provider.of(context); + _calendarController.selectedRange = PickerDateRange(model.fromDate, model.toDate); - _calendarController.selectedRange = PickerDateRange(model.fromDate,model.toDate); + return widget.designType == CalendarDesignType.designV2 ? _buildDesignV2() : _buildDefaultUI(); + } + Widget _buildDefaultUI() { return Padding( padding: EdgeInsets.symmetric(horizontal: 0.h), child: Column( @@ -57,14 +96,8 @@ class _DateRangeSelectorState extends State { builder: (_, model, __) => selectionChip(model), ).paddingOnly(bottom: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: false, - ), - padding: EdgeInsets.all( - 16.h - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), + padding: EdgeInsets.all(16.h), child: Column( children: [ Row( @@ -72,13 +105,8 @@ class _DateRangeSelectorState extends State { fromDateComponent(), Text( LocaleKeys.to.tr(context: context), - style: TextStyle( - color: AppColors.calenderTextColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - letterSpacing: -.2 - ), - ).paddingSymmetrical(24.h,0.h), + style: TextStyle(color: AppColors.calenderTextColor, fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -.2), + ).paddingSymmetrical(24.h, 0.h), toDateComponent(), ], ), @@ -126,8 +154,7 @@ class _DateRangeSelectorState extends State { ), startRangeSelectionColor: AppColors.primaryRedColor, endRangeSelectionColor: AppColors.primaryRedColor, - rangeSelectionColor: - AppColors.primaryRedColor.withOpacity(0.1), + rangeSelectionColor: AppColors.primaryRedColor.withOpacity(0.1), rangeTextStyle: TextStyle( fontFamily: "Poppins", ), @@ -144,8 +171,7 @@ class _DateRangeSelectorState extends State { fontWeight: FontWeight.bold, ), ), - onSelectionChanged: - (DateRangePickerSelectionChangedArgs args) { + onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { print("the value is ${args.value}"); if (args.value is PickerDateRange) { final PickerDateRange range = args.value; @@ -153,11 +179,10 @@ class _DateRangeSelectorState extends State { end = range.endDate; model.fromDate = start; model.toDate = end; - if(end == null) { - scheduleMicrotask((){ + if (end == null) { + scheduleMicrotask(() { model.resetCurrentlySelectedRange(); }); - } } }, @@ -169,7 +194,7 @@ class _DateRangeSelectorState extends State { Row( children: [ Consumer( - builder: (_, model, __) => Visibility( + builder: (_, model, __) => Visibility( visible: (model.fromDate != null || model.toDate != null), child: Expanded( child: Row( @@ -192,7 +217,9 @@ class _DateRangeSelectorState extends State { height: 56.h, ), ), - SizedBox(width: 16.h,) + SizedBox( + width: 16.h, + ) ], ), ), @@ -210,7 +237,6 @@ class _DateRangeSelectorState extends State { textColor: AppColors.textGreenColor, icon: AppAssets.reminder_bell, iconColor: AppColors.textGreenColor, - height: 56.h, ), ), @@ -221,11 +247,620 @@ class _DateRangeSelectorState extends State { ); } + Widget _buildDesignV2() { + return ChangeNotifierProvider( + create: (_) => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()), + child: Consumer( + builder: (context, calendarModel, child) { + // Check if current language is Arabic + final bool isArabic = context.locale.languageCode == 'ar'; + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Conditionally show calendar toggle + if (widget.showCalendarToggle) + Selector( + selector: (_, model) => model.getSelectedTabIndex, + builder: (context, selectedIndex, child) => CustomTabBar( + activeBackgroundColor: AppColors.secondaryLightRedColor, + activeTextColor: AppColors.primaryRedColor, + initialIndex: selectedIndex, + tabs: [CustomTabBarModel(null, LocaleKeys.gregorianDate.tr()), CustomTabBarModel(null, LocaleKeys.hijriDate.tr())], + onTabChange: (index) { + final calendarModel = Provider.of(context, listen: false); + calendarModel.setTabIndex(index); + + // Notify parent widget about calendar type change (e.g., AuthenticationViewModel) + final isGregorian = index == 0; + widget.onCalendarTypeChanged?.call(isGregorian); + print('📅 Calendar type changed: ${isGregorian ? "Gregorian" : "Hijri"}'); + + // Reset view mode and Hijri display when switching calendar types + setState(() { + _viewMode = PickerViewMode.date; + _currentHijriDisplay = null; + }); + }, + ), + ), + if (widget.showCalendarToggle) + SizedBox( + height: 16.w, + ), + + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + // Show custom header for both calendar types + _buildCalendarHeader(isArabic, calendarModel), + // Wrap all views in SizedBox for consistent height + SizedBox( + height: 272.h, // Fixed height for calendar body only (no header) + child: Material( + color: AppColors.whiteColor, + // Rebuild when view mode or calendar type changes + key: ValueKey('${calendarModel.getSelectedTabIndex}-$_viewMode'), + child: _viewMode == PickerViewMode.month + ? _buildMonthPicker(isArabic, calendarModel) + : _viewMode == PickerViewMode.year + ? _buildYearPicker(isArabic, calendarModel) + : (calendarModel.isHijri ? _buildHijriCalendar(isArabic) : _buildGregorianCalendar(isArabic)), + ), + ), + ], + ), + ), + // Only Select button for V2 (no cancel button) + CustomButton( + text: LocaleKeys.select.tr(context: context), + onPressed: () { + // Don't reset calendar type - keep user's preference + Navigator.of(context).pop(); + // For single date selection, pass the same date as both start and end + widget.onRangeSelected(model.fromDate, model.fromDate); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, + textColor: AppColors.whiteColor, + height: 56.h, + ).paddingOnly(top: 24.h), + // Bottom image from ui2.png + SizedBox(height: 16.h), + ], + ), + ); + }, + ), + ); + } + + // Build Gregorian calendar (Design V2 - Single Date Selection) + Widget _buildGregorianCalendar(bool isArabic) { + return SfDateRangePicker( + controller: _calendarController, + selectionMode: DateRangePickerSelectionMode.single, + showNavigationArrow: false, + headerHeight: 0, + // Hide built-in header, use custom header + backgroundColor: AppColors.whiteColor, + monthViewSettings: DateRangePickerMonthViewSettings( + viewHeaderStyle: DateRangePickerViewHeaderStyle( + backgroundColor: AppColors.whiteColor, + textStyle: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.textColor, + ), + ), + showTrailingAndLeadingDates: false, + dayFormat: "EEE", + ), + // Custom cell builder for square border selection + cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { + if (cellDetails.date != DateTime(0)) { + final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; + final isSelected = _calendarController.selectedDate != null && + cellDetails.date.day == _calendarController.selectedDate!.day && + cellDetails.date.month == _calendarController.selectedDate!.month && + cellDetails.date.year == _calendarController.selectedDate!.year; + + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Text( + cellDetails.date.day.toString(), + style: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), + ), + ), + ); + } + return Container(); + }, + selectionShape: DateRangePickerSelectionShape.rectangle, + selectionRadius: 8.h, + selectionColor: Colors.transparent, + selectionTextStyle: TextStyle( + fontFamily: "Poppins", + color: AppColors.textColor, + fontWeight: FontWeight.w600, + ), + todayHighlightColor: Colors.transparent, + monthCellStyle: DateRangePickerMonthCellStyle( + textStyle: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + ), + todayTextStyle: TextStyle( + fontFamily: "Poppins", + color: AppColors.textColor, + fontWeight: FontWeight.bold, + ), + ), + onViewChanged: (DateRangePickerViewChangedArgs args) { + // Trigger rebuild when month changes + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + }, + onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { + if (args.value is DateTime) { + setState(() { + start = args.value; + end = args.value; + }); + model.updateSelectedDate(args.value); + } + }, + ); + } + + // Build Hijri calendar (Design V2 - Single Date Selection) + // Note: SfDateRangePicker doesn't have built-in Hijri support + // Using Gregorian calendar with Hijri date conversion in the model + Widget _buildHijriCalendar(bool isArabic) { + final calendarModel = Provider.of(context, listen: false); + + return SfDateRangePicker( + controller: _calendarController, + selectionMode: DateRangePickerSelectionMode.single, + showNavigationArrow: false, + headerHeight: 0, + backgroundColor: AppColors.whiteColor, + monthViewSettings: DateRangePickerMonthViewSettings( + viewHeaderStyle: DateRangePickerViewHeaderStyle( + backgroundColor: AppColors.whiteColor, + textStyle: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.textColor, + ), + ), + showTrailingAndLeadingDates: false, + dayFormat: "EEE", + ), + cellBuilder: (BuildContext context, DateRangePickerCellDetails cellDetails) { + if (cellDetails.date != DateTime(0)) { + // Use cached Hijri conversion + final dateKey = '${cellDetails.date.year}-${cellDetails.date.month}-${cellDetails.date.day}'; + final hijriDate = _hijriCache.putIfAbsent(dateKey, () => calendarModel.gregorianToHijri(cellDetails.date)); + + final isToday = cellDetails.date.day == DateTime.now().day && cellDetails.date.month == DateTime.now().month && cellDetails.date.year == DateTime.now().year; + final isSelected = _calendarController.selectedDate != null && + cellDetails.date.day == _calendarController.selectedDate!.day && + cellDetails.date.month == _calendarController.selectedDate!.month && + cellDetails.date.year == _calendarController.selectedDate!.year; + + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isSelected ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Text( + hijriDate.day.toString(), + style: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + fontWeight: isToday ? FontWeight.bold : (isSelected ? FontWeight.w600 : FontWeight.normal), + ), + ), + ); + } + return Container(); + }, + selectionShape: DateRangePickerSelectionShape.rectangle, + selectionRadius: 8.h, + selectionColor: Colors.transparent, + selectionTextStyle: TextStyle( + fontFamily: "Poppins", + color: AppColors.textColor, + fontWeight: FontWeight.w600, + ), + todayHighlightColor: Colors.transparent, + monthCellStyle: DateRangePickerMonthCellStyle( + textStyle: TextStyle( + fontFamily: "Poppins", + fontSize: 12.f, + color: AppColors.textColor, + ), + todayTextStyle: TextStyle( + fontFamily: "Poppins", + color: AppColors.textColor, + fontWeight: FontWeight.bold, + ), + ), + onViewChanged: (DateRangePickerViewChangedArgs args) { + // Clear cache when month changes to avoid stale data + _hijriCache.clear(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + }, + onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { + if (args.value is DateTime) { + setState(() { + start = args.value; + end = args.value; + }); + model.updateSelectedDate(args.value); + } + }, + ); + } + + // Build unified calendar header for both Gregorian and Hijri + Widget _buildCalendarHeader(bool isArabic, DateRangCalenderModel calendarModel) { + final displayedDate = _calendarController.displayDate ?? DateTime.now(); + + String monthText; + String yearText; + + if (calendarModel.isHijri) { + // Use tracked Hijri display date if available, otherwise convert from displayed Gregorian date + final hijriDate = _currentHijriDisplay ?? calendarModel.gregorianToHijri(displayedDate); + final monthName = calendarModel.getMonthNameLocalized(hijriDate.month, isArabic); + monthText = monthName; + yearText = hijriDate.year.toString(); + } else { + final monthNames = isArabic + ? ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + monthText = monthNames[displayedDate.month - 1]; + yearText = displayedDate.year.toString(); + } + + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: SizedBox( + height: 40.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + // Month text - clickable + GestureDetector( + onTap: () { + setState(() { + _viewMode = _viewMode == PickerViewMode.month ? PickerViewMode.date : PickerViewMode.month; + }); + }, + child: Text( + monthText, + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.primaryRedColor, + fontFamily: "Poppins", + ), + textAlign: isArabic ? TextAlign.right : TextAlign.left, + ), + ), + SizedBox(width: 8.h), + // Year text - clickable + GestureDetector( + onTap: () { + setState(() { + _viewMode = _viewMode == PickerViewMode.year ? PickerViewMode.date : PickerViewMode.year; + }); + }, + child: Text( + yearText, + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.w600, + letterSpacing: -0.46, + color: AppColors.primaryRedColor, + fontFamily: "Poppins", + ), + textAlign: isArabic ? TextAlign.right : TextAlign.left, + ), + ), + ], + ), + Row( + children: [ + IconButton( + icon: Icon(Icons.chevron_left, color: AppColors.primaryRedColor), + onPressed: () { + if (_viewMode == PickerViewMode.date) { + _calendarController.backward!(); + // Update tracked Hijri display when navigating months + if (calendarModel.isHijri) { + setState(() { + _currentHijriDisplay = null; // Will recalculate on next build + }); + } + } else if (_viewMode == PickerViewMode.year) { + // Navigate years backward by 12 + final currentDate = _calendarController.displayDate ?? DateTime.now(); + if (calendarModel.isHijri) { + // For Hijri, convert back and forth + final hijriDate = _currentHijriDisplay ?? calendarModel.gregorianToHijri(currentDate); + try { + final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year - 12); + final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); + _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _currentHijriDisplay = newHijriDate; + } catch (e) { + // Fallback + _calendarController.displayDate = DateTime(currentDate.year - 10, currentDate.month, 1); + } + } else { + _calendarController.displayDate = DateTime(currentDate.year - 12, currentDate.month, 1); + } + setState(() {}); + } + }, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + ), + SizedBox(width: 8.h), + IconButton( + icon: Icon(Icons.chevron_right, color: AppColors.primaryRedColor), + onPressed: () { + if (_viewMode == PickerViewMode.date) { + _calendarController.forward!(); + // Update tracked Hijri display when navigating months + if (calendarModel.isHijri) { + setState(() { + _currentHijriDisplay = null; // Will recalculate on next build + }); + } + } else if (_viewMode == PickerViewMode.year) { + // Navigate years forward by 12 + final currentDate = _calendarController.displayDate ?? DateTime.now(); + if (calendarModel.isHijri) { + // For Hijri, convert back and forth + final hijriDate = _currentHijriDisplay ?? calendarModel.gregorianToHijri(currentDate); + try { + final newHijriDate = HijriGregDate(day: 1, month: hijriDate.month, year: hijriDate.year + 12); + final newGregorianDate = calendarModel.hijriToGregorian(newHijriDate); + _calendarController.displayDate = DateTime(newGregorianDate.year, newGregorianDate.month, 1); + _currentHijriDisplay = newHijriDate; + } catch (e) { + // Fallback + _calendarController.displayDate = DateTime(currentDate.year + 10, currentDate.month, 1); + } + } else { + _calendarController.displayDate = DateTime(currentDate.year + 12, currentDate.month, 1); + } + setState(() {}); + } + }, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + ), + ], + ), + ], + ), + ), + ); + } + + // Build month picker grid + Widget _buildMonthPicker(bool isArabic, DateRangCalenderModel calendarModel) { + final displayedDate = _calendarController.displayDate ?? DateTime.now(); + final currentDate = DateTime.now(); + + int currentMonth; + int currentYear; + int displayedYear; + + if (calendarModel.isHijri) { + // For Hijri calendar + final hijriCurrent = calendarModel.gregorianToHijri(currentDate); + final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + currentMonth = hijriCurrent.month; + currentYear = hijriCurrent.year; + displayedYear = hijriDisplayed.year; + } else { + // For Gregorian calendar + currentMonth = currentDate.month; + currentYear = currentDate.year; + displayedYear = displayedDate.year; + } + + List monthNames; + if (calendarModel.isHijri) { + monthNames = isArabic ? DateRangCalenderModel.hijriMonthNamesArabic : DateRangCalenderModel.hijriMonthNamesEnglish; + } else { + monthNames = isArabic ? DateRangCalenderModel.gregorianMonthNamesArabic : DateRangCalenderModel.gregorianMonthNamesEnglish; + } + + return GridView.builder( + padding: EdgeInsets.all(8.h), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 2.5, crossAxisSpacing: 8.h, mainAxisSpacing: 8.h), + itemCount: 12, + itemBuilder: (context, index) { + final monthIndex = index + 1; + final isCurrentMonth = currentMonth == monthIndex && currentYear == displayedYear; + + return GestureDetector( + onTap: () { + // Update the displayed month based on calendar type + if (calendarModel.isHijri) { + // Convert Hijri month to Gregorian date + try { + final hijriDate = HijriGregDate(day: 1, month: monthIndex, year: displayedYear); + final gregorianDate = calendarModel.hijriToGregorian(hijriDate); + _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + // Track the Hijri month/year for accurate header display + _currentHijriDisplay = hijriDate; + // Clear cache since we changed the month + _hijriCache.clear(); + } catch (e) { + // Fallback if conversion fails + _calendarController.displayDate = DateTime(displayedDate.year, monthIndex, 1); + } + } else { + // Gregorian month - direct update + _calendarController.displayDate = DateTime(displayedYear, monthIndex, 1); + } + + // Switch back to date view + setState(() { + _viewMode = PickerViewMode.date; + }); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isCurrentMonth ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Center( + child: Text( + monthNames[index], + style: TextStyle( + fontFamily: "Poppins", + fontSize: 14.f, + color: AppColors.textColor, + fontWeight: isCurrentMonth ? FontWeight.w600 : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + }, + ); + } + + // Build year picker grid + Widget _buildYearPicker(bool isArabic, DateRangCalenderModel calendarModel) { + final displayedDate = _calendarController.displayDate ?? DateTime.now(); + final currentYear = DateTime.now().year; + + int displayedYear; + int currentDisplayYear; + int startYear; + + if (calendarModel.isHijri) { + // For Hijri calendar + final hijriDisplayed = calendarModel.gregorianToHijri(displayedDate); + final hijriCurrent = calendarModel.gregorianToHijri(DateTime.now()); + displayedYear = hijriDisplayed.year; + currentDisplayYear = hijriCurrent.year; + startYear = (displayedYear ~/ 12) * 12; + } else { + // For Gregorian calendar + displayedYear = displayedDate.year; + currentDisplayYear = currentYear; + startYear = (displayedYear ~/ 12) * 12; + } + + return GridView.builder( + padding: EdgeInsets.all(8.h), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 2.5, + crossAxisSpacing: 8.h, + mainAxisSpacing: 8.h, + ), + itemCount: 12, + itemBuilder: (context, index) { + final year = startYear + index; + final isCurrentYear = currentDisplayYear == year; + + return GestureDetector( + onTap: () { + // Update the displayed year based on calendar type + if (calendarModel.isHijri) { + // Convert Hijri year to Gregorian date + try { + final currentHijriMonth = _currentHijriDisplay?.month ?? calendarModel.gregorianToHijri(displayedDate).month; + final hijriDate = HijriGregDate(day: 1, month: currentHijriMonth, year: year); + final gregorianDate = calendarModel.hijriToGregorian(hijriDate); + _calendarController.displayDate = DateTime(gregorianDate.year, gregorianDate.month, 1); + // Track the Hijri month/year for accurate header display + _currentHijriDisplay = hijriDate; + } catch (e) { + // Fallback if conversion fails + _calendarController.displayDate = DateTime(displayedDate.year, displayedDate.month, 1); + } + } else { + // Gregorian year - direct update + _calendarController.displayDate = DateTime(year, displayedDate.month, 1); + } + + // Switch back to date view + setState(() { + _viewMode = PickerViewMode.date; + }); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.h), + border: isCurrentYear ? Border.all(color: AppColors.primaryRedColor, width: 2) : null, + color: Colors.transparent, + ), + child: Center( + child: Text( + year.toString(), + style: TextStyle( + fontFamily: "Poppins", + fontSize: 14.f, + color: AppColors.textColor, + fontWeight: isCurrentYear ? FontWeight.w600 : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + }, + ); + } + fromDateComponent() { return Consumer( builder: (_, model, __) { - return displayDate(LocaleKeys.startDate.tr(context: context), - model.getDateString(model.fromDate), model.fromDate == null); + return displayDate(LocaleKeys.startDate.tr(context: context), model.getDateString(model.fromDate), model.fromDate == null); }, ); } @@ -233,21 +868,16 @@ class _DateRangeSelectorState extends State { toDateComponent() { return Consumer( builder: (_, model, __) { - return displayDate(LocaleKeys.endDate.tr(context: context), - model.getDateString(model.toDate), model.toDate == null); + return displayDate(LocaleKeys.endDate.tr(context: context), model.getDateString(model.toDate), model.toDate == null); }, ); } displayDate(String label, String? date, bool isNotSelected) => Expanded( - child: Row( + child: Row( spacing: 12.h, children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.rangeCalendar, - iconColor: isNotSelected ? AppColors.borderOnlyColor: AppColors.blackColor , - height: 24, - width: 24), + Utils.buildSvgWithAssets(icon: AppAssets.rangeCalendar, iconColor: isNotSelected ? AppColors.borderOnlyColor : AppColors.blackColor, height: 24, width: 24), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -271,7 +901,7 @@ class _DateRangeSelectorState extends State { ) ], ), - ); + ); selectionChip(DateRangeSelectorRangeViewModel model) { return Row( @@ -279,59 +909,46 @@ class _DateRangeSelectorState extends State { children: [ AppCustomChipWidget( labelText: LocaleKeys.thisWeek.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.WEEKLY - ? AppColors.primaryRedColor.withOpacity(0.1) - : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( - color: model.currentlySelectedRange == Range.WEEKLY - ? AppColors.primaryRedBorderColor - : AppColors.chipBorderColorOpacity20, + color: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, width: 1, ), borderRadius: BorderRadius.circular(10)), - ).onPress((){ + ).onPress(() { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.WEEKLY; model.calculateDatesFromRange(); }), AppCustomChipWidget( labelText: LocaleKeys.lastMonth.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH - ? AppColors.primaryRedColor.withOpacity(0.1) - : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( - color: model.currentlySelectedRange == Range.LAST_MONTH - ? AppColors.primaryRedBorderColor - : AppColors.chipBorderColorOpacity20, + color: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, width: 1, ), borderRadius: BorderRadius.circular(10)), - ).onPress((){ + ).onPress(() { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.LAST_MONTH; model.calculateDatesFromRange(); }), AppCustomChipWidget( labelText: LocaleKeys.lastSixMonths.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH - ? AppColors.primaryRedColor.withOpacity(0.1) - : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( - color: model.currentlySelectedRange == Range.LAST_6MONTH - ? AppColors.primaryRedBorderColor - : AppColors.chipBorderColorOpacity20, + color: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, width: 1, ), borderRadius: BorderRadius.circular(10)), isEnglishOnly: true, - ).onPress((){ + ).onPress(() { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.LAST_6MONTH; model.calculateDatesFromRange(); - }), AppCustomChipWidget( richText: Row( @@ -341,23 +958,18 @@ class _DateRangeSelectorState extends State { ], ), labelText: "${LocaleKeys.year.tr(context: context)} ${model.getCurrentYear}", - backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR - ? AppColors.primaryRedColor.withOpacity(0.1) - : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( - color: model.currentlySelectedRange == Range.THIS_YEAR - ? AppColors.primaryRedBorderColor - : AppColors.chipBorderColorOpacity20, + color: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, width: 1, ), borderRadius: BorderRadius.circular(10)), isEnglishOnly: true, - ).onPress((){ + ).onPress(() { _calendarController.selectedRange = null; model.currentlySelectedRange = Range.THIS_YEAR; model.calculateDatesFromRange(); - }), ], ); diff --git a/lib/widgets/date_range_selector/models/calendar_design_type.dart b/lib/widgets/date_range_selector/models/calendar_design_type.dart new file mode 100644 index 00000000..5cbc8129 --- /dev/null +++ b/lib/widgets/date_range_selector/models/calendar_design_type.dart @@ -0,0 +1,5 @@ +enum CalendarDesignType { + defaultUI, + designV2, +} + diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart new file mode 100644 index 00000000..54c23a72 --- /dev/null +++ b/lib/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart @@ -0,0 +1,555 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +// ==================== Hijri Date Classes ==================== + +/// Represents a Hijri date with day, month, and year components. +class HijriGregDate { + final int day; + final int month; + final int year; + + HijriGregDate({required this.day, required this.month, required this.year}) + : assert(day >= 1 && day <= 30, 'Day must be between 1 and 30'), + assert(month >= 1 && month <= 12, 'Month must be between 1 and 12'), + assert(year > 0, 'Year must be positive'); + + @override + String toString() => '$day/$month/$year H'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is HijriGregDate && other.day == day && other.month == month && other.year == year; + } + + @override + int get hashCode => day.hashCode ^ month.hashCode ^ year.hashCode; +} + +/// Utility class for converting between Hijri and Gregorian calendars. +class HijriGregConverter { + static const int _hijriEpoch = 1948439; + + static HijriGregDate gregorianToHijri(DateTime gregorianDate) { + int julianDay = _gregorianToJulian(gregorianDate); + return _julianToHijri(julianDay); + } + + static DateTime hijriToGregorian(HijriGregDate hijriDate) { + int julianDay = _hijriToJulian(hijriDate); + return _julianToGregorian(julianDay); + } + + static int _gregorianToJulian(DateTime date) { + int year = date.year; + int month = date.month; + int day = date.day; + + if (month <= 2) { + year--; + month += 12; + } + + int a = year ~/ 100; + int b = 2 - a + (a ~/ 4); + + return (365.25 * (year + 4716)).floor() + (30.6001 * (month + 1)).floor() + day + b - 1524; + } + + static DateTime _julianToGregorian(int julianDay) { + int a = julianDay + 32044; + int b = (4 * a + 3) ~/ 146097; + int c = a - (146097 * b) ~/ 4; + int d = (4 * c + 3) ~/ 1461; + int e = c - (1461 * d) ~/ 4; + int m = (5 * e + 2) ~/ 153; + + int day = e - (153 * m + 2) ~/ 5 + 1; + int month = m + 3 - 12 * (m ~/ 10); + int year = 100 * b + d - 4800 + m ~/ 10; + + return DateTime(year, month, day); + } + + static HijriGregDate _julianToHijri(int julianDay) { + int daysSinceEpoch = julianDay - _hijriEpoch; + int hijriYear = (daysSinceEpoch * 33 ~/ 10631) + 1; + + int yearStartJulian = _hijriYearStartJulian(hijriYear); + while (yearStartJulian > julianDay) { + hijriYear--; + yearStartJulian = _hijriYearStartJulian(hijriYear); + } + while (yearStartJulian + _hijriYearLength(hijriYear) <= julianDay) { + hijriYear++; + yearStartJulian = _hijriYearStartJulian(hijriYear); + } + + int dayOfYear = julianDay - yearStartJulian + 1; + int month = 1; + int dayInMonth = dayOfYear; + + while (month <= 12) { + int monthLength = _hijriMonthLength(hijriYear, month); + if (dayInMonth <= monthLength) { + break; + } + dayInMonth -= monthLength; + month++; + } + + if (dayInMonth < 1) dayInMonth = 1; + if (dayInMonth > 30) dayInMonth = 30; + + return HijriGregDate(day: dayInMonth, month: month, year: hijriYear); + } + + static int _hijriToJulian(HijriGregDate hijriDate) { + int yearStart = _hijriYearStartJulian(hijriDate.year); + int dayOfYear = 0; + + for (int i = 1; i < hijriDate.month; i++) { + dayOfYear += _hijriMonthLength(hijriDate.year, i); + } + + dayOfYear += hijriDate.day - 1; + return yearStart + dayOfYear; + } + + static int _hijriYearStartJulian(int hijriYear) { + if (hijriYear <= 1) return _hijriEpoch; + + int totalDays = 0; + for (int year = 1; year < hijriYear; year++) { + totalDays += _hijriYearLength(year); + } + + return _hijriEpoch + totalDays; + } + + static int _hijriYearLength(int year) { + return _isHijriLeapYear(year) ? 355 : 354; + } + + static int _hijriMonthLength(int year, int month) { + if (month <= 0 || month > 12) return 30; + + const List monthLengths = [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29]; + int length = monthLengths[month - 1]; + + if (month == 12 && _isHijriLeapYear(year)) { + length = 30; + } + + return length; + } + + static bool _isHijriLeapYear(int year) { + if (year <= 0) return false; + const leapYears = [2, 5, 7, 10, 13, 16, 18, 21, 24, 26, 29]; + int yearInCycle = ((year - 1) % 30) + 1; + return leapYears.contains(yearInCycle); + } + + static int getHijriMonthLength(int year, int month) { + return _hijriMonthLength(year, month); + } + + static bool isHijriLeapYear(int year) { + return _isHijriLeapYear(year); + } +} + +// ==================== Main Model ==================== + +enum CalendarType { + gregorian, + hijri, +} + +class DateRangCalenderModel extends ChangeNotifier { + final AppState appState; + final ErrorHandlerService navigationService; + final DialogService dialogService; + + // Static variable to persist calendar type preference across instances + static CalendarType _persistedCalendarType = CalendarType.gregorian; + static int _persistedTabIndex = 0; + + DateRangCalenderModel({required this.appState, required this.navigationService, required this.dialogService}) { + // Initialize from persisted state + _initializeFromPersistedState(); + } + + int _selectedTabIndex = 0; + CalendarType _calendarType = CalendarType.gregorian; + + void _initializeFromPersistedState() { + // Load from static persisted values + _selectedTabIndex = _persistedTabIndex; + _calendarType = _persistedCalendarType; + } + + // Umm al-Qura calendar adjustment (in days) + // This offset corrects the astronomical calculation to match the official Saudi calendar + static const int _ummAlQuraOffset = -1; + + // Hijri month names in English (short form) + static const List _hijriMonthNamesShort = [ + 'Muh', + 'Saf', + 'Rab I', + 'Rab II', + 'Jum I', + 'Jum II', + 'Raj', + 'Sha', + 'Ram', + 'Shaw', + 'Dhu Q', + 'Dhu H', + ]; + + // Hijri month names in English (full form) - public for month picker + static const List hijriMonthNamesEnglish = [ + 'Muharram', + 'Safar', + 'Rabi\' I', + 'Rabi\' II', + 'Jumada I', + 'Jumada II', + 'Rajab', + 'Sha\'ban', + 'Ramadan', + 'Shawwal', + 'Dhu al-Qi\'dah', + 'Dhu al-Hijjah', + ]; + + // Hijri month names in Arabic (short form) + static const List _hijriMonthNamesShortArabic = [ + 'محرم', + 'صفر', + 'ربيع الأول', + 'ربيع الثاني', + 'جمادى الأولى', + 'جمادى الثانية', + 'رجب', + 'شعبان', + 'رمضان', + 'شوال', + 'ذو القعدة', + 'ذو الحجة', + ]; + + // Hijri month names in Arabic (full form) - public for month picker + static const List hijriMonthNamesArabic = [ + 'محرم', + 'صفر', + 'ربيع الأول', + 'ربيع الثاني', + 'جمادى الأولى', + 'جمادى الثانية', + 'رجب', + 'شعبان', + 'رمضان', + 'شوال', + 'ذو القعدة', + 'ذو الحجة', + ]; + + // Gregorian month names (short form) + static const List _gregorianMonthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + // Gregorian month names in English (full form) - public for month picker + static const List gregorianMonthNamesEnglish = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + // Gregorian month names in Arabic + static const List _gregorianMonthNamesArabic = [ + 'يناير', + 'فبراير', + 'مارس', + 'أبريل', + 'مايو', + 'يونيو', + 'يوليو', + 'أغسطس', + 'سبتمبر', + 'أكتوبر', + 'نوفمبر', + 'ديسمبر', + ]; + + // Gregorian month names in Arabic (full form) - public for month picker + static const List gregorianMonthNamesArabic = [ + 'يناير', + 'فبراير', + 'مارس', + 'أبريل', + 'مايو', + 'يونيو', + 'يوليو', + 'أغسطس', + 'سبتمبر', + 'أكتوبر', + 'نوفمبر', + 'ديسمبر', + ]; + + // Weekday names in English (short form) + static const List _weekdayNamesEnglish = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + ]; + + // Weekday names in Arabic (short form) + static const List _weekdayNamesArabic = [ + 'أحد', + 'اثنين', + 'ثلاثاء', + 'أربعاء', + 'خميس', + 'جمعة', + 'سبت', + ]; + + int get getSelectedTabIndex => _selectedTabIndex; + + CalendarType get calendarType => _calendarType; + + set selectedTabIndex(int value) { + _selectedTabIndex = value; + _calendarType = value == 0 ? CalendarType.gregorian : CalendarType.hijri; + notifyListeners(); + } + + void setTabIndex(int index) { + _selectedTabIndex = index; + _calendarType = index == 0 ? CalendarType.gregorian : CalendarType.hijri; + // Persist the selection for next time the widget opens + _persistCalendarTypePreference(); + notifyListeners(); + } + + void _persistCalendarTypePreference() { + // Store the preference in static variables so it persists when widget reopens + _persistedTabIndex = _selectedTabIndex; + _persistedCalendarType = _calendarType; + } + + bool get isGregorian => _calendarType == CalendarType.gregorian; + + bool get isHijri => _calendarType == CalendarType.hijri; + + /// Reset to Gregorian and clear persisted preference + /// Call this only when explicitly needed (e.g., logout) + void reset({bool force = false}) { + if (force) { + // Force reset - clear both instance and persisted state + _selectedTabIndex = 0; + _calendarType = CalendarType.gregorian; + _persistedTabIndex = 0; + _persistedCalendarType = CalendarType.gregorian; + notifyListeners(); + } + // If not forcing, keep the user's preference (persisted state remains) + } + + /// Clear persisted calendar type preference (reset to Gregorian on next open) + static void clearPersistedPreference() { + _persistedTabIndex = 0; + _persistedCalendarType = CalendarType.gregorian; + } + + // ==================== Conversion Methods ==================== + + /// Converts Gregorian DateTime to Hijri date with Umm al-Qura correction + HijriGregDate gregorianToHijri(DateTime gregorianDate) { + // Apply Umm al-Qura offset for accurate Saudi calendar + final adjustedDate = gregorianDate.add(Duration(days: _ummAlQuraOffset)); + return HijriGregConverter.gregorianToHijri(adjustedDate); + } + + /// Converts Hijri date to Gregorian DateTime with Umm al-Qura correction + DateTime hijriToGregorian(HijriGregDate hijriDate) { + final gregorianDate = HijriGregConverter.hijriToGregorian(hijriDate); + // Reverse the Umm al-Qura offset + return gregorianDate.subtract(Duration(days: _ummAlQuraOffset)); + } + + // ==================== Date Formatting Methods ==================== + + /// Formats a date based on the current calendar type + /// Returns formatted string like "14 Apr,26" for Gregorian or "14 Muh,1447" for Hijri + String formatDate(DateTime? date) { + if (date == null) return "-"; + + if (isGregorian) { + return formatGregorianDate(date); + } else { + return formatHijriDate(date); + } + } + + /// Formats a Gregorian date + /// Format: "dd MMM,yy" (e.g., "14 Apr,26") + String formatGregorianDate(DateTime date) { + String year = date.year.toString().substring(2); + String month = _gregorianMonthNames[date.month - 1]; + String day = date.day.toString(); + return '$day $month,$year'; + } + + /// Formats a date as Hijri + /// Format: "dd MMM,yyyy" (e.g., "14 Muh,1447") + String formatHijriDate(DateTime gregorianDate) { + HijriGregDate hijriDate = gregorianToHijri(gregorianDate); + String month = _hijriMonthNamesShort[hijriDate.month - 1]; + return '${hijriDate.day} $month,${hijriDate.year}'; + } + + /// Gets month name based on current calendar type + String getMonthName(int month) { + if (isGregorian) { + return _gregorianMonthNames[month - 1]; + } else { + return _hijriMonthNamesShort[month - 1]; + } + } + + /// Gets month name in Arabic based on current calendar type + String getMonthNameArabic(int month) { + if (isGregorian) { + return _gregorianMonthNamesArabic[month - 1]; + } else { + return _hijriMonthNamesShortArabic[month - 1]; + } + } + + /// Gets month name with language support + String getMonthNameLocalized(int month, bool isArabic) { + return isArabic ? getMonthNameArabic(month) : getMonthName(month); + } + + /// Gets weekday name in English + String getWeekdayName(int weekday) { + // weekday: 1 = Monday, 7 = Sunday in DateTime + // Convert to Sunday = 0 index + int index = weekday % 7; + return _weekdayNamesEnglish[index]; + } + + /// Gets weekday name in Arabic + String getWeekdayNameArabic(int weekday) { + // weekday: 1 = Monday, 7 = Sunday in DateTime + // Convert to Sunday = 0 index + int index = weekday % 7; + return _weekdayNamesArabic[index]; + } + + /// Gets weekday name with language support + String getWeekdayNameLocalized(int weekday, bool isArabic) { + return isArabic ? getWeekdayNameArabic(weekday) : getWeekdayName(weekday); + } + + /// Converts a date string to display format based on calendar type + String formatDateString(String? dateString) { + if (dateString == null || dateString.isEmpty) return "-"; + + try { + DateTime date = DateTime.parse(dateString); + return formatDate(date); + } catch (e) { + return "-"; + } + } + + // ==================== Date Range Methods ==================== + + /// Formats a date range based on current calendar type + /// Returns formatted string like "14 Apr,26 - 20 Apr,26" for Gregorian + String formatDateRange(DateTime? startDate, DateTime? endDate) { + if (startDate == null || endDate == null) return "-"; + + String start = formatDate(startDate); + String end = formatDate(endDate); + return '$start - $end'; + } + + /// Gets the current date in the selected calendar format + String getCurrentDateFormatted() { + return formatDate(DateTime.now()); + } + + // ==================== Calendar Type Specific Helpers ==================== + + /// Gets the year based on calendar type + int getCurrentYear() { + if (isGregorian) { + return DateTime.now().year; + } else { + return gregorianToHijri(DateTime.now()).year; + } + } + + /// Gets formatted year string based on calendar type + String getFormattedYear(DateTime date) { + if (isGregorian) { + return date.year.toString(); + } else { + return gregorianToHijri(date).year.toString(); + } + } + + /// Checks if a year is leap year based on calendar type + bool isLeapYear(int year) { + if (isGregorian) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + } else { + return HijriGregConverter.isHijriLeapYear(year); + } + } + + /// Gets the number of days in a month based on calendar type + int getDaysInMonth(int year, int month) { + if (isGregorian) { + return DateTime(year, month + 1, 0).day; + } else { + return HijriGregConverter.getHijriMonthLength(year, month); + } + } +} diff --git a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart index 3f67b6a6..e92d0817 100644 --- a/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart +++ b/lib/widgets/date_range_selector/viewmodel/date_range_view_model.dart @@ -85,4 +85,12 @@ class DateRangeSelectorRangeViewModel extends ChangeNotifier { notifyListeners(); } + /// Update both fromDate and toDate (for single date selection in V2) + void updateSelectedDate(DateTime? date) { + _fromDate = date; + _toDate = date; + _currentlySelectedRange = null; + notifyListeners(); + } + } diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index b1a0883f..63870e25 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -1,6 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:hijri_gregorian_calendar/hijri_gregorian_calendar.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -9,9 +9,14 @@ import 'package:hmg_patient_app_new/core/enums.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/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_kesy.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/models/calendar_design_type.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart'; +import 'dart:ui' as ui; class TextInputWidget extends StatelessWidget { final String labelText; @@ -222,26 +227,26 @@ class TextInputWidget extends StatelessWidget { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.whiteColor), child: GestureDetector( onTap: () async { - bool isGregorian = true; - final picked = await showHijriGregBottomSheet(context, - isShowTimeSlots: true, - switcherIcon: Utils.buildSvgWithAssets(icon: AppAssets.language, width: 24.h, height: 24.h), - language: appState.getLanguageCode()!, - initialDate: DateTime.now(), + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.selectDate.tr(context: context), + context, + child: DateRangeSelector( + onRangeSelected: (DateTime? start, DateTime? end) { + var picked = start.toString().split(" ").first; + if (picked != null && onChange != null) { + onChange!(picked.toString()); + } + }, + designType: CalendarDesignType.designV2, showCalendarToggle: isHideSwitcher == true ? false : true, - // fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", - fontFamily: "Poppins", - okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), - cancelWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)), - onCalendarTypeChanged: (bool value) { - isGregorian = value; - }); - if (picked != null && onChange != null) { - if (onCalendarTypeChanged != null) { - onCalendarTypeChanged!.call(isGregorian); - } - onChange!(picked.toString()); - } + onCalendarTypeChanged: onCalendarTypeChanged, + ), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () { + // Don't reset calendar type - persist user's preference + }, + ); }, child: Utils.buildSvgWithAssets(icon: isArrowTrailing ? AppAssets.arrow_down : AppAssets.calendar), ), @@ -299,13 +304,11 @@ class TextInputWidget extends StatelessWidget { return Builder( builder: (context) { return Directionality( - textDirection: isArabic ? TextDirection.rtl : TextDirection.ltr, + textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr, child: Localizations.override( - context: context, locale: const Locale('en', 'US'), // Force English locale for TextField child: TextField( - hintLocales: const [Locale('en', 'US')], enabled: isEnable, scrollPadding: EdgeInsets.zero, @@ -314,7 +317,7 @@ class TextInputWidget extends StatelessWidget { readOnly: isReadOnly, textAlignVertical: TextAlignVertical.top, textAlign: isArabic ? TextAlign.right : TextAlign.left, - textDirection: TextDirection.ltr, + textDirection: ui.TextDirection.ltr, onChanged: onChange, focusNode: focusNode ?? _focusNode, autofocus: autoFocus, @@ -346,7 +349,6 @@ class TextInputWidget extends StatelessWidget { isDense: true, hintText: hintText, hintStyle: TextStyle( - fontFamily: isArabic ? 'CairoArabic' : 'Poppins', fontSize: 14.f, height: 21 / 16, @@ -356,7 +358,7 @@ class TextInputWidget extends StatelessWidget { ), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, isBold: true), - contentPadding: EdgeInsets.only(right:isArabic ? 10.w :0), + contentPadding: EdgeInsets.only(right: isArabic ? 10.w : 0), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, @@ -484,4 +486,3 @@ class _ThousandSeparatorInputFormatter extends TextInputFormatter { ); } } - diff --git a/pubspec.yaml b/pubspec.yaml index 7f14f57c..4d853184 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,7 +71,8 @@ dependencies: firebase_analytics: ^11.5.1 firebase_crashlytics: ^4.3.8 jiffy: ^6.4.3 - hijri_gregorian_calendar: ^0.1.1 +# hijri_gregorian_calendar: +# path: /Users/aamir/StudioProjects/hijri_gregorian_calendar sms_otp_auto_verify: ^2.2.0 web: any flutter_staggered_animations: ^1.1.1