Merge pull request 'Calendar Design Changes' (#250) from dev_aamir into master

Reviewed-on: #250
dev_aamir
khansultan1 9 hours ago
commit 2c45063291

@ -895,6 +895,7 @@
"monitor": "مراقبة",
"noSpecialResult": "لا توجد نتائج خاصة",
"setTheDateRange": "تعيين النطاق الزمني",
"selectDate": "اختر التاريخ",
"historyFlowchart": "مخطط تدفق التاريخ",
"to": "إلى",
"startDate": "تاريخ البدء",

@ -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",

@ -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<NotificationService>(() => NotificationServiceImp(
flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin,
@ -172,8 +172,12 @@ class AppDependencies {
getIt.registerLazySingleton<MonthlyReportRepo>(() => MonthlyReportRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ActivePrescriptionsRepo>(() => ActivePrescriptionsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<TermsConditionsRepo>(() => TermsConditionsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerFactory<TermsConditionsViewModel>(() => TermsConditionsViewModel(termsConditionsRepo: getIt<TermsConditionsRepo>(), errorHandlerService: getIt<ErrorHandlerService>(),
),);
getIt.registerFactory<TermsConditionsViewModel>(
() => TermsConditionsViewModel(
termsConditionsRepo: getIt<TermsConditionsRepo>(),
errorHandlerService: getIt<ErrorHandlerService>(),
),
);
getIt.registerLazySingleton<MonthlyReportsRepo>(() => MonthlyReportsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<QrParkingRepo>(() => QrParkingRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<NotificationsRepo>(() => NotificationsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
@ -233,10 +237,10 @@ class AppDependencies {
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel(
cacheService: getIt<CacheService>(),
profileSettingsRepo: getIt<ProfileSettingsRepo>(),
errorHandlerService: getIt<ErrorHandlerService>(),
));
cacheService: getIt<CacheService>(),
profileSettingsRepo: getIt<ProfileSettingsRepo>(),
errorHandlerService: getIt<ErrorHandlerService>(),
));
getIt.registerLazySingleton<DateRangeSelectorRangeViewModel>(() => DateRangeSelectorRangeViewModel());
@ -312,22 +316,12 @@ class AppDependencies {
getIt.registerLazySingleton<NotificationsViewModel>(() => NotificationsViewModel(notificationsRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<HealthTrackersViewModel>(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ActivePrescriptionsViewModel>(
() => ActivePrescriptionsViewModel(
errorHandlerService: getIt(),
activePrescriptionsRepo: getIt()
),
);
getIt.registerFactory<QrParkingViewModel>(
() => QrParkingViewModel(
qrParkingRepo: getIt<QrParkingRepo>(),
errorHandlerService: getIt<ErrorHandlerService>(),
cacheService: getIt<CacheService>(),
),
);
getIt.registerLazySingleton<ActivePrescriptionsViewModel>(() => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt()));
getIt.registerFactory<QrParkingViewModel>(() => QrParkingViewModel(qrParkingRepo: getIt<QrParkingRepo>(), errorHandlerService: getIt<ErrorHandlerService>(), cacheService: getIt<CacheService>()));
getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<DateRangCalenderModel>(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()));
}
}

@ -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({

@ -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("=========");
}
}
});

@ -907,6 +907,7 @@ class CodegenLoader extends AssetLoader{
"monitor": "مراقبة",
"noSpecialResult": "لا توجد نتائج خاصة",
"setTheDateRange": "تعيين النطاق الزمني",
"selectDate": "اختر التاريخ",
"historyFlowchart": "مخطط تدفق التاريخ",
"to": "إلى",
"startDate": "تاريخ البدء",
@ -2627,6 +2628,7 @@ static const Map<String,dynamic> _en_US = {
"monitor": "Monitor",
"noSpecialResult": "No Special Results",
"setTheDateRange": "Set The Date Range",
"selectDate": "Select Date",
"historyFlowchart": "History FlowChart",
"to": "to",
"startDate": "Start Date",

@ -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';

@ -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';

@ -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<WeatherMonitorViewModel>(
create: (_) => getIt.get<WeatherMonitorViewModel>(),
), ChangeNotifierProvider<DateRangCalenderModel>(
create: (_) => getIt.get<DateRangCalenderModel>(),
)
], child: MyApp()),
), // Wrap your app

@ -73,7 +73,7 @@ class _RegisterNew extends State<RegisterNewStep2> {
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<RegisterNewStep2> {
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();

@ -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<DateRangeSelector> createState() => _DateRangeSelectorState();
}
enum PickerViewMode {
date,
month,
year,
}
class _DateRangeSelectorState extends State<DateRangeSelector> {
late DateRangePickerController _calendarController;
DateTime? start;
DateTime? end;
late DateRangeSelectorRangeViewModel model;
PickerViewMode _viewMode = PickerViewMode.date;
// Cache for Hijri conversions to avoid repeated calculations
final Map<String, HijriGregDate> _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<DateRangeSelector> {
Widget build(BuildContext context) {
model = Provider.of<DateRangeSelectorRangeViewModel>(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<DateRangeSelector> {
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<DateRangeSelector> {
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<DateRangeSelector> {
),
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<DateRangeSelector> {
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<DateRangeSelector> {
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<DateRangeSelector> {
Row(
children: [
Consumer<DateRangeSelectorRangeViewModel>(
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<DateRangeSelector> {
height: 56.h,
),
),
SizedBox(width: 16.h,)
SizedBox(
width: 16.h,
)
],
),
),
@ -210,7 +237,6 @@ class _DateRangeSelectorState extends State<DateRangeSelector> {
textColor: AppColors.textGreenColor,
icon: AppAssets.reminder_bell,
iconColor: AppColors.textGreenColor,
height: 56.h,
),
),
@ -221,11 +247,620 @@ class _DateRangeSelectorState extends State<DateRangeSelector> {
);
}
Widget _buildDesignV2() {
return ChangeNotifierProvider(
create: (_) => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt()),
child: Consumer<DateRangCalenderModel>(
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<DateRangCalenderModel, int>(
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<DateRangCalenderModel>(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<DateRangCalenderModel>(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<String> 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<DateRangeSelectorRangeViewModel>(
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<DateRangeSelector> {
toDateComponent() {
return Consumer<DateRangeSelectorRangeViewModel>(
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<DateRangeSelector> {
)
],
),
);
);
selectionChip(DateRangeSelectorRangeViewModel model) {
return Row(
@ -279,59 +909,46 @@ class _DateRangeSelectorState extends State<DateRangeSelector> {
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<DateRangeSelector> {
],
),
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();
}),
],
);

@ -0,0 +1,5 @@
enum CalendarDesignType {
defaultUI,
designV2,
}

@ -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<int> 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<String> _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<String> 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<String> _hijriMonthNamesShortArabic = [
'محرم',
'صفر',
'ربيع الأول',
'ربيع الثاني',
'جمادى الأولى',
'جمادى الثانية',
'رجب',
'شعبان',
'رمضان',
'شوال',
'ذو القعدة',
'ذو الحجة',
];
// Hijri month names in Arabic (full form) - public for month picker
static const List<String> hijriMonthNamesArabic = [
'محرم',
'صفر',
'ربيع الأول',
'ربيع الثاني',
'جمادى الأولى',
'جمادى الثانية',
'رجب',
'شعبان',
'رمضان',
'شوال',
'ذو القعدة',
'ذو الحجة',
];
// Gregorian month names (short form)
static const List<String> _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<String> gregorianMonthNamesEnglish = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
// Gregorian month names in Arabic
static const List<String> _gregorianMonthNamesArabic = [
'يناير',
'فبراير',
'مارس',
'أبريل',
'مايو',
'يونيو',
'يوليو',
'أغسطس',
'سبتمبر',
'أكتوبر',
'نوفمبر',
'ديسمبر',
];
// Gregorian month names in Arabic (full form) - public for month picker
static const List<String> gregorianMonthNamesArabic = [
'يناير',
'فبراير',
'مارس',
'أبريل',
'مايو',
'يونيو',
'يوليو',
'أغسطس',
'سبتمبر',
'أكتوبر',
'نوفمبر',
'ديسمبر',
];
// Weekday names in English (short form)
static const List<String> _weekdayNamesEnglish = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
// Weekday names in Arabic (short form)
static const List<String> _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);
}
}
}

@ -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();
}
}

@ -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 {
);
}
}

@ -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

Loading…
Cancel
Save