Merge pull request 'Calendar Design Changes' (#250) from dev_aamir into master
Reviewed-on: #250dev_aamir
commit
2c45063291
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue