diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 385d3e76..eaaa6848 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -562,18 +562,118 @@ class AuthenticationViewModel extends ChangeNotifier { } void onDobChange(String? date) { - if (calenderType == CalenderEnum.hijri) { - // 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()); - } else { - dobController.text = Utils.formatDateToDisplay(date!); - dob = Utils.formatDateForApi(date); + if (date == null || date.isEmpty) { + debugPrint('onDobChange: date is null or empty'); + return; } - notifyListeners(); + + try { + if (calenderType == CalenderEnum.hijri) { + // Use DateRangCalenderModel for accurate Umm al-Qura conversion + final calendarModel = DateRangCalenderModel( + appState: _appState, + navigationService: _errorHandlerService, + dialogService: _dialogService, + ); + + // Parse the Gregorian date with validation + DateTime gregorianDate; + try { + gregorianDate = DateTime.parse(date); + } catch (parseError) { + debugPrint('onDobChange: Failed to parse date "$date" - $parseError'); + // Try to handle common date format issues + gregorianDate = _tryParseDateFlexibly(date); + } + + // Convert to Hijri + final hijriDate = calendarModel.gregorianToHijri(gregorianDate); + + // Validate Hijri date components + if (hijriDate.year <= 0 || hijriDate.month <= 0 || hijriDate.day <= 0) { + debugPrint('onDobChange: Invalid Hijri date components - Year: ${hijriDate.year}, Month: ${hijriDate.month}, Day: ${hijriDate.day}'); + return; + } + + // Create DateTime from Hijri components for display + final hijriDateTimeForController = DateTime(hijriDate.year, hijriDate.month, hijriDate.day); + + // Format for API (use original Gregorian date) + dob = Utils.formatDateForApi(date); + + // Format for display (use Hijri) + dobController.text = Utils.formatHijriDateToDisplay(hijriDateTimeForController.toIso8601String()); + + } else { + // Gregorian calendar mode + // Validate the date can be parsed + try { + DateTime.parse(date); + } catch (parseError) { + debugPrint('onDobChange: Failed to parse date "$date" - $parseError'); + // Try flexible parsing to validate + _tryParseDateFlexibly(date); + } + + // Format for display + dobController.text = Utils.formatDateToDisplay(date); + + // Format for API + dob = Utils.formatDateForApi(date); + } + + clearDobError(); + notifyListeners(); + + } catch (e, stackTrace) { + debugPrint('onDobChange: Unexpected error processing date "$date" - $e'); + debugPrint('Stack trace: $stackTrace'); + + // Set error state if needed + // You can add error handling UI feedback here + } + } + + /// Helper method to handle flexible date parsing for common formats + DateTime _tryParseDateFlexibly(String dateString) { + // Remove extra whitespace + dateString = dateString.trim(); + + // Try standard ISO format first + try { + return DateTime.parse(dateString); + } catch (_) {} + + // Try common formats with regex + final patterns = [ + RegExp(r'^(\d{4})-(\d{1,2})-(\d{1,2})$'), // yyyy-M-d or yyyy-MM-dd + RegExp(r'^(\d{1,2})/(\d{1,2})/(\d{4})$'), // d/M/yyyy or dd/MM/yyyy + RegExp(r'^(\d{4})/(\d{1,2})/(\d{1,2})$'), // yyyy/M/d or yyyy/MM/dd + ]; + + for (var pattern in patterns) { + final match = pattern.firstMatch(dateString); + if (match != null) { + try { + if (pattern.pattern.startsWith(r'^\d{4}')) { + // Year first formats + final year = int.parse(match.group(1)!); + final month = int.parse(match.group(2)!); + final day = int.parse(match.group(3)!); + return DateTime(year, month, day); + } else { + // Day first formats + final day = int.parse(match.group(1)!); + final month = int.parse(match.group(2)!); + final year = int.parse(match.group(3)!); + return DateTime(year, month, day); + } + } catch (_) {} + } + } + + // If all else fails, throw descriptive error + throw FormatException('Unable to parse date: "$dateString". Expected format: yyyy-MM-dd'); } Future loadCountriesData() async { @@ -583,15 +683,21 @@ class AuthenticationViewModel extends ChangeNotifier { } void onMaritalStatusChange(String? status) { - maritalStatus = MaritalStatusTypeExtension.fromType(status)!; - clearMaritalStatusError(); - notifyListeners(); + final newMaritalStatus = MaritalStatusTypeExtension.fromType(status); + if (newMaritalStatus != null) { + maritalStatus = newMaritalStatus; + clearMaritalStatusError(); + notifyListeners(); + } } void onGenderChange(String? status) { - genderType = GenderTypeExtension.fromType(status)!; - clearGenderError(); - notifyListeners(); + final newGender = GenderTypeExtension.fromType(status); + if (newGender != null) { + genderType = newGender; + clearGenderError(); + notifyListeners(); + } } void clearEmailInput() { diff --git a/lib/features/blood_donation/blood_donation_view_model.dart b/lib/features/blood_donation/blood_donation_view_model.dart index 3da82a75..2b978f0a 100644 --- a/lib/features/blood_donation/blood_donation_view_model.dart +++ b/lib/features/blood_donation/blood_donation_view_model.dart @@ -73,8 +73,11 @@ class BloodDonationViewModel extends ChangeNotifier { } void onGenderChange(String? status) { - selectedGender = GenderTypeExtension.fromType(status)!; - notifyListeners(); + final newGender = GenderTypeExtension.fromType(status); + if (newGender != null) { + selectedGender = newGender; + notifyListeners(); + } } setSelectedBloodGroup(BloodGroupListModel bloodGroup) { diff --git a/test/date_conversion_test.dart b/test/date_conversion_test.dart new file mode 100644 index 00000000..46c9cc3f --- /dev/null +++ b/test/date_conversion_test.dart @@ -0,0 +1,365 @@ +import 'dart:developer' as dev; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; + +/// Comprehensive test to analyze date conversion issues between Gregorian and Hijri calendars +/// This test simulates the onDobChange method logic and tests 200 years of dates +void main() { + group('Date Conversion Analysis - 200 Years', () { + // Test results storage + final List results = []; + final List errors = []; + + test('Test Gregorian to Hijri conversion for 200 years', () { + print('\n========================================'); + print('GREGORIAN TO HIJRI CONVERSION TEST'); + print('Testing dates from 1925 to 2125 (200 years)'); + print('========================================\n'); + + int totalDates = 0; + int successfulConversions = 0; + int failedConversions = 0; + + // Test dates from 1925 to 2125 (200 years) + for (int year = 1925; year <= 2125; year++) { + // Test all 12 months + for (int month = 1; month <= 12; month++) { + // Test key days in each month (1st, 15th, last day) + final daysToTest = [1, 15, _getLastDayOfMonth(year, month)]; + + for (int day in daysToTest) { + totalDates++; + + try { + // Create Gregorian date + final gregorianDate = DateTime(year, month, day); + final isoString = gregorianDate.toIso8601String(); + + // Test parsing (this is where the error occurs) + final parsedDate = DateTime.parse(isoString); + + // Test Hijri conversion + final hijriDate = HijriGregConverter.gregorianToHijri(parsedDate); + + // Test creating DateTime from Hijri components + final hijriDateTime = DateTime(hijriDate.year, hijriDate.month, hijriDate.day); + + // Test formatting methods + final apiFormat = _formatDateForApi(isoString); + final displayFormat = _formatDateToDisplay(isoString); + final hijriDisplayFormat = _formatHijriDateToDisplay(hijriDateTime.toIso8601String()); + + // Store successful result + results.add(DateConversionResult( + gregorianDate: gregorianDate, + isoString: isoString, + hijriYear: hijriDate.year, + hijriMonth: hijriDate.month, + hijriDay: hijriDate.day, + apiFormat: apiFormat, + displayFormat: displayFormat, + hijriDisplayFormat: hijriDisplayFormat, + success: true, + )); + + successfulConversions++; + + } catch (e) { + failedConversions++; + final errorMsg = 'ERROR on $year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}: $e'; + errors.add(errorMsg); + print(errorMsg); + + results.add(DateConversionResult( + gregorianDate: DateTime(year, month, day), + isoString: 'FAILED', + hijriYear: 0, + hijriMonth: 0, + hijriDay: 0, + apiFormat: 'FAILED', + displayFormat: 'FAILED', + hijriDisplayFormat: 'FAILED', + success: false, + errorMessage: e.toString(), + )); + } + } + } + + // Print progress every 10 years + if (year % 10 == 0) { + print('Processed up to year $year... (Success: $successfulConversions, Failed: $failedConversions)'); + } + } + + // Print summary + print('\n========================================'); + print('TEST SUMMARY'); + print('========================================'); + print('Total dates tested: $totalDates'); + print('Successful conversions: $successfulConversions'); + print('Failed conversions: $failedConversions'); + print('Success rate: ${(successfulConversions / totalDates * 100).toStringAsFixed(2)}%'); + print('========================================\n'); + + // Print all errors + if (errors.isNotEmpty) { + print('\n========================================'); + print('ALL ERRORS (${errors.length} total):'); + print('========================================'); + for (var error in errors) { + print(error); + } + } + + // Print sample successful conversions + if (results.where((r) => r.success).isNotEmpty) { + print('\n========================================'); + print('SAMPLE SUCCESSFUL CONVERSIONS (First 5):'); + print('========================================'); + int count = 0; + for (var result in results.where((r) => r.success)) { + if (count >= 5) break; + print('Gregorian: ${result.gregorianDate}'); + print(' ISO: ${result.isoString}'); + print(' Hijri: ${result.hijriYear}-${result.hijriMonth}-${result.hijriDay}'); + print(' API Format: ${result.apiFormat}'); + print(' Display Format: ${result.displayFormat}'); + print(' Hijri Display: ${result.hijriDisplayFormat}'); + print('---'); + count++; + } + } + + // Test should fail if there are any errors + expect(failedConversions, 0, reason: 'Found $failedConversions failed conversions'); + }); + + test('Test specific edge cases', () { + print('\n========================================'); + print('EDGE CASES TEST'); + print('========================================\n'); + + final edgeCases = [ + DateTime(1900, 1, 1), // Very old date + DateTime(1970, 1, 1), // Unix epoch + DateTime(2000, 1, 1), // Y2K + DateTime(2000, 2, 29), // Leap year + DateTime(2001, 2, 28), // Non-leap year + DateTime(2024, 12, 31), // Recent date + DateTime(2025, 1, 1), // Near future + DateTime(2100, 12, 31), // Far future + DateTime(2124, 2, 29), // Leap year in far future + DateTime(2125, 12, 31), // End of test range + ]; + + for (var testDate in edgeCases) { + try { + final isoString = testDate.toIso8601String(); + final parsedDate = DateTime.parse(isoString); + final hijriDate = HijriGregConverter.gregorianToHijri(parsedDate); + + print('✓ ${testDate.toString().split(' ')[0]} -> Hijri: ${hijriDate.year}-${hijriDate.month}-${hijriDate.day}'); + } catch (e) { + print('✗ ${testDate.toString().split(' ')[0]} -> ERROR: $e'); + fail('Edge case failed: $testDate - $e'); + } + } + }); + + test('Test DateRangCalenderModel with Umm al-Qura offset', () { + print('\n========================================'); + print('UMM AL-QURA OFFSET TEST'); + print('========================================\n'); + + // We can't fully test DateRangCalenderModel without full context, + // but we can test the conversion logic + + final testDates = [ + DateTime(2024, 1, 1), + DateTime(2024, 6, 15), + DateTime(2024, 12, 31), + DateTime(2025, 1, 1), + ]; + + for (var testDate in testDates) { + try { + // Simulate the Umm al-Qura offset (-1 day) + final adjustedDate = testDate.subtract(const Duration(days: 1)); + final hijriDate = HijriGregConverter.gregorianToHijri(adjustedDate); + + print('Original: ${testDate.toString().split(' ')[0]}'); + print(' Adjusted (-1 day): ${adjustedDate.toString().split(' ')[0]}'); + print(' Hijri: ${hijriDate.year}-${hijriDate.month}-${hijriDate.day}'); + print('---'); + } catch (e) { + print('ERROR on ${testDate.toString().split(' ')[0]}: $e'); + fail('Umm al-Qura offset test failed: $testDate - $e'); + } + } + }); + + test('Test problematic date formats', () { + print('\n========================================'); + print('PROBLEMATIC DATE FORMATS TEST'); + print('========================================\n'); + + final problematicFormats = [ + '2024-01-01T00:00:00.000', + '2024-01-01T00:00:00.000Z', + '2024-01-01T00:00:00', + '2024-01-01', + '2024-1-1', + '2024-01-1', + '2024-1-01', + ]; + + for (var format in problematicFormats) { + try { + final parsed = DateTime.parse(format); + print('✓ Format "$format" parsed successfully: ${parsed.toString()}'); + } catch (e) { + print('✗ Format "$format" FAILED to parse: $e'); + } + } + }); + }); +} + +// Helper function to get last day of month +int _getLastDayOfMonth(int year, int month) { + if (month == 12) { + return DateTime(year + 1, 1, 1).subtract(const Duration(days: 1)).day; + } + return DateTime(year, month + 1, 1).subtract(const Duration(days: 1)).day; +} + +// Replicate Utils methods for testing +String _formatDateForApi(String isoDateString) { + try { + final dateTime = DateTime.parse(isoDateString); + final year = dateTime.year.toString(); + final month = dateTime.month.toString().padLeft(2, '0'); + final day = dateTime.day.toString().padLeft(2, '0'); + return '$day/$month/$year'; + } catch (e) { + return 'ERROR: $e'; + } +} + +String _formatDateToDisplay(String isoDateString) { + try { + final dateTime = DateTime.parse(isoDateString); + final day = dateTime.day.toString().padLeft(2, '0'); + final year = dateTime.year.toString(); + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + final month = monthNames[dateTime.month - 1]; + return '$day $month, $year'; + } catch (e) { + return 'ERROR: $e'; + } +} + +String _formatHijriDateToDisplay(String hijriDateString) { + try { + final datePart = hijriDateString.split("T").first; + final parts = datePart.split('-'); + if (parts.length != 3) return 'ERROR: Invalid format'; + + final day = parts[2].padLeft(2, '0'); + final year = parts[0]; + + const hijriMonthNames = [ + 'Muharram', + 'Safar', + 'Rabi I', + 'Rabi II', + 'Jumada I', + 'Jumada II', + 'Rajab', + 'Sha\'ban', + 'Ramadan', + 'Shawwal', + 'Dhu al-Qi\'dah', + 'Dhu al-Hijjah' + ]; + final monthIndex = int.tryParse(parts[1]) ?? 1; + final month = hijriMonthNames[monthIndex - 1]; + + return '$day $month, $year'; + } catch (e) { + return 'ERROR: $e'; + } +} + +// Stub for HijriGregConverter (since we're testing the logic) +class HijriGregConverter { + static HijriGregDate gregorianToHijri(DateTime gregorianDate) { + // This is a simplified conversion for testing + // In reality, this would use the actual Hijri calendar library + + // Basic astronomical calculation (not exact Umm al-Qura) + final julianDay = _gregorianToJulianDay(gregorianDate); + final hijriDate = _julianDayToHijri(julianDay); + + return hijriDate; + } + + static int _gregorianToJulianDay(DateTime date) { + int a = (14 - date.month) ~/ 12; + int y = date.year + 4800 - a; + int m = date.month + 12 * a - 3; + + return date.day + (153 * m + 2) ~/ 5 + 365 * y + y ~/ 4 - y ~/ 100 + y ~/ 400 - 32045; + } + + static HijriGregDate _julianDayToHijri(int julianDay) { + // Basic conversion (simplified for testing) + int l = julianDay - 1948440 + 10632; + int n = (l - 1) ~/ 10631; + l = l - 10631 * n + 354; + int j = ((10985 - l) ~/ 5316) * ((50 * l) ~/ 17719) + (l ~/ 5670) * ((43 * l) ~/ 15238); + l = l - ((30 - j) ~/ 15) * ((17719 * j) ~/ 50) - (j ~/ 16) * ((15238 * j) ~/ 43) + 29; + int month = (24 * l) ~/ 709; + int day = l - (709 * month) ~/ 24; + int year = 30 * n + j - 30; + + return HijriGregDate(year: year, month: month, day: day); + } +} + +class HijriGregDate { + final int year; + final int month; + final int day; + + HijriGregDate({required this.year, required this.month, required this.day}); +} + +// Data class to store test results +class DateConversionResult { + final DateTime gregorianDate; + final String isoString; + final int hijriYear; + final int hijriMonth; + final int hijriDay; + final String apiFormat; + final String displayFormat; + final String hijriDisplayFormat; + final bool success; + final String? errorMessage; + + DateConversionResult({ + required this.gregorianDate, + required this.isoString, + required this.hijriYear, + required this.hijriMonth, + required this.hijriDay, + required this.apiFormat, + required this.displayFormat, + required this.hijriDisplayFormat, + required this.success, + this.errorMessage, + }); +} +