Skip to content

Conversation

@AliAkrem
Copy link
Owner

@AliAkrem AliAkrem commented Dec 20, 2025

Description

  • Add year selection page with UI for choosing academic year
  • Implement redirect logic to enforce year selection after login

Types of changes

  • Bug Fix (non-breaking change which fixes an issue)
  • New Feature (non-breaking change which adds functionality)
  • Others (any other types not listed above)

Checklist

Summary by CodeRabbit

  • New Features
    • Academic year selection page added and accessible from Settings; users can choose and save their academic year.
  • Improvements
    • Profile and enrollment displays respect the selected academic year; profile caching is now year-scoped.
    • Settings shows current selected year and lets users change it.
  • Bug Fixes
    • Logout clears the selected year; safer handling when clearing selection fails.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

Adds an academic year selection feature: a YearSelectionPage, YearSelectionService persisting chosen year, year-scoped profile caching, modifications to profile loading and student repository to respect selected year, settings UI to pick year, minor routing import, and logout clearing of the saved year.

Changes

Cohort / File(s) Summary
Routing
lib/config/routes/app_router.dart
Added import and a new yearSelection route constant and GoRoute rendering YearSelectionPage. No existing redirect/auth gating logic changed.
Core service
lib/core/services/year_selection_service.dart
New YearSelectionService persisting/reading/clearing selected academic year (id and code) via SharedPreferences; async API: save/get/has/clear.
Authentication
lib/features/auth/presentation/bloc/auth_bloc.dart
On logout, attempts to clear saved academic year via YearSelectionService; errors are logged but do not break logout flow.
Profile models
lib/features/profile/data/models/academic_year.dart, lib/features/profile/data/models/student_detailed_info.dart
AcademicYear.copyWith({int?, String?}) added; StudentDetailedInfo.fromJson now defaults transportPaye to false when absent/null.
Profile repository & cache
lib/features/profile/data/repositories/student_repository_impl.dart, lib/features/profile/data/services/profile_cache_service.dart
StudentRepositoryImpl now accepts YearSelectionService and prefers a manually selected year when present; persists chosen year. ProfileCacheService changed to year-scoped keys: cache/get/lastUpdated now require yearId; clearCache accepts optional yearId to clear specific or all year-scoped entries.
Profile presentation
lib/features/profile/presentation/bloc/profile_bloc.dart, lib/features/profile/presentation/pages/profile_page.dart
ProfileBloc load flow reordered to resolve current year first, use year-scoped cache lookups and a year-aware cache fallback on error. ProfilePage selects enrollment matching the current academic year instead of the latest.
Settings + Year selection UI
lib/features/settings/presentation/pages/settings_page.dart, lib/features/settings/presentation/pages/year_selection_page.dart
SettingsPage converted to StatefulWidget displaying selected year and navigating to selection. New YearSelectionPage lists enrollments, allows radio selection, saves via YearSelectionService, clears profile cache, and restarts app; includes loading/error/empty states and retry handling.
Localization
lib/l10n/app_en.arb, lib/l10n/app_ar.arb, lib/l10n/app_localizations*.dart
Added five new localization keys and corresponding getters: selectYearDescription, pleaseSelectYear, noEnrollmentsFound, errorLoadingData, and notSelected with English and Arabic entries and generated localization getters.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Router as App Router
    participant Auth as AuthBloc
    participant YearSvc as YearSelectionService
    participant YearPage as YearSelectionPage
    participant EnrollRepo as EnrollmentRepo
    participant ProfileCache as ProfileCacheService
    participant ProfileRepo as StudentRepositoryImpl

    User->>Router: navigate app
    Router->>Auth: query auth state
    Auth-->>Router: authenticated
    Router->>YearSvc: hasSelectedYear()?
    alt no selected year
        Router->>YearPage: navigate to YearSelectionPage
        YearPage->>EnrollRepo: fetch user enrollments
        EnrollRepo-->>YearPage: enrollments
        User->>YearPage: select year + confirm
        YearPage->>YearSvc: saveSelectedYear(id, code)
        YearSvc-->>YearPage: saved
        YearPage->>ProfileCache: clearCache(yearId) / clearCache()
        YearPage->>Router: restart/redirect to app
    else selected year exists
        Router->>ProfileRepo: getCurrentAcademicYear()
        ProfileRepo->>YearSvc: getSelectedYearId() (may be used)
        ProfileRepo-->>Router: academic year
        Router->>ProfileCache: getCachedProfileData(yearId)
        alt cache hit
            ProfileCache-->>Router: cached profile
        else cache miss
            Router->>ProfileRepo: fetch profile/enrollments live
            ProfileRepo-->>Router: profile data
            Router->>ProfileCache: cacheProfileData(data, yearId)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • lib/features/profile/data/repositories/student_repository_impl.dart (year precedence, persistence)
    • lib/features/profile/data/services/profile_cache_service.dart (key naming, clearCache behavior)
    • lib/features/profile/presentation/bloc/profile_bloc.dart (reordered flow and nested error fallback)
    • lib/features/settings/presentation/pages/year_selection_page.dart (selection/save/clear/restart logic)

Possibly related PRs

Poem

🐰
I hopped through lists of years today,
Chose one to keep, then tucked it away,
Cached profiles now wear a yearly hat,
Settings show the pick — imagine that!
Small feet, big changes — hop, restart, hooray!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature added: an academic year selection capability with a dedicated page and related infrastructure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch academic-year

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/features/profile/presentation/bloc/profile_bloc.dart (1)

119-148: State emission order causes UI flicker.

ProfileLoading is emitted at line 148 after ProfileLoaded is emitted from cache (lines 136-145). This causes the UI to:

  1. Show cached data (ProfileLoaded)
  2. Immediately show loading spinner (ProfileLoading)
  3. Show fresh data (ProfileLoaded again)

This results in a visual flicker. Consider restructuring to emit ProfileLoading first, or skip emitting it when cache is available since you're refreshing in the background anyway.

🔎 Proposed fix - Option A: Emit loading first, then cache
+      emit(ProfileLoading());
+
       // Try to load cached profile for this specific year
       final cachedProfileData = await cacheService.getCachedProfileData(
         academicYear.id,
       );
       if (cachedProfileData != null) {
         // ... emit ProfileLoaded from cache
       }
-
-      emit(ProfileLoading());
🔎 Proposed fix - Option B: Skip loading state when cache exists (background refresh)
       if (cachedProfileData != null) {
         // ... existing cache logic
         emit(
           ProfileLoaded(
             // ...
           ),
         );
-      }
-
-      emit(ProfileLoading());
+      } else {
+        emit(ProfileLoading());
+      }
🧹 Nitpick comments (5)
lib/features/settings/presentation/pages/settings_page.dart (1)

20-35: Consider dependency injection for YearSelectionService.

The service initialization and state management logic is correct. However, consider whether YearSelectionService should be a singleton or injected via dependency injection for better testability and consistency with other services in the codebase.

lib/core/services/year_selection_service.dart (1)

3-33: Consider singleton pattern for shared state consistency.

The service implementation is correct, but since it manages global user preference state via SharedPreferences, consider implementing it as a singleton to prevent potential inconsistencies from multiple instances and reduce unnecessary SharedPreferences.getInstance() calls.

🔎 Example singleton implementation
 class YearSelectionService {
+  static final YearSelectionService _instance = YearSelectionService._internal();
+  factory YearSelectionService() => _instance;
+  YearSelectionService._internal();
+
   static const String _selectedYearIdKey = 'selected_academic_year_id';
   static const String _selectedYearCodeKey = 'selected_academic_year_code';
lib/features/settings/presentation/pages/year_selection_page.dart (1)

83-85: Consider alternatives to full app restart.

Calling Restart.restartApp() is a heavy operation. Consider whether you can achieve the same effect by:

  • Clearing relevant caches and resetting navigation state
  • Using a state management solution to propagate the year change
  • Only reloading affected screens

This would provide a smoother user experience.

lib/config/routes/app_router.dart (1)

64-93: Repeated YearSelectionService instantiation on every redirect.

A new YearSelectionService() instance is created on lines 81 and 88 for every navigation event. Since YearSelectionService likely interacts with SharedPreferences, this adds unnecessary overhead.

Consider caching the service instance or making it a singleton:

🔎 Proposed fix - Cache the service
 class AppRouter {
   // Route names as static constants
   static const String splash = 'splash';
   // ...
+  
+  // Cached year selection service
+  static final YearSelectionService _yearSelectionService = YearSelectionService();
 
   // ...
 
-      redirect: (context, state) async {
+      redirect: (context, state) async {
         // ...
 
         // Authenticated but on login page - check year selection
         if (authState is AuthSuccess && isLoginRoute) {
-          final yearService = YearSelectionService();
-          final hasSelectedYear = await yearService.hasSelectedYear();
+          final hasSelectedYear = await _yearSelectionService.hasSelectedYear();
           return hasSelectedYear ? dashboardPath : yearSelectionPath;
         }
 
         // Authenticated - check if year is selected for protected routes
         if (authState is AuthSuccess && !isYearSelectionRoute) {
-          final yearService = YearSelectionService();
-          final hasSelectedYear = await yearService.hasSelectedYear();
+          final hasSelectedYear = await _yearSelectionService.hasSelectedYear();
           if (!hasSelectedYear) {
             return yearSelectionPath;
           }
         }
lib/features/profile/data/services/profile_cache_service.dart (1)

65-89: Consider handling _currentYearKey when clearing a specific year.

The clearCache method has two modes: clearing a specific year (lines 69-73) or clearing all profile caches (lines 74-82). When clearing a specific year, if that year happens to be the current year tracked in _currentYearKey, the key becomes stale—it points to data that no longer exists in cache.

While this is a minor edge case (the stale reference will self-correct on the next cache operation), consider either:

  1. Checking if yearId == _currentYearKey and clearing _currentYearKey when clearing that specific year, or
  2. Documenting this behavior if it's intentional
🔎 Optional fix to maintain consistency
 if (yearId != null) {
   // Clear specific year cache
   await prefs.remove(_getProfileKey(yearId));
   await prefs.remove(_getLastUpdatedKey(yearId));
+  // Clear current year reference if we're clearing the current year
+  final currentYear = prefs.getInt(_currentYearKey);
+  if (currentYear == yearId) {
+    await prefs.remove(_currentYearKey);
+  }
 } else {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4be1ac1 and d6a50cd.

📒 Files selected for processing (16)
  • lib/config/routes/app_router.dart (5 hunks)
  • lib/core/services/year_selection_service.dart (1 hunks)
  • lib/features/auth/presentation/bloc/auth_bloc.dart (2 hunks)
  • lib/features/profile/data/models/academic_year.dart (1 hunks)
  • lib/features/profile/data/models/student_detailed_info.dart (1 hunks)
  • lib/features/profile/data/repositories/student_repository_impl.dart (2 hunks)
  • lib/features/profile/data/services/profile_cache_service.dart (3 hunks)
  • lib/features/profile/presentation/bloc/profile_bloc.dart (4 hunks)
  • lib/features/profile/presentation/pages/profile_page.dart (3 hunks)
  • lib/features/settings/presentation/pages/settings_page.dart (2 hunks)
  • lib/features/settings/presentation/pages/year_selection_page.dart (1 hunks)
  • lib/l10n/app_ar.arb (1 hunks)
  • lib/l10n/app_en.arb (1 hunks)
  • lib/l10n/app_localizations.dart (1 hunks)
  • lib/l10n/app_localizations_ar.dart (1 hunks)
  • lib/l10n/app_localizations_en.dart (1 hunks)
🔇 Additional comments (16)
lib/features/auth/presentation/bloc/auth_bloc.dart (1)

84-90: LGTM!

Year selection clearing is properly integrated into the logout flow with consistent error handling that matches the existing cache clearing patterns.

lib/l10n/app_ar.arb (1)

738-757: LGTM!

The new localization keys are properly formatted with descriptions and consistent with the existing pattern in the file.

lib/features/profile/presentation/pages/profile_page.dart (2)

52-68: LGTM!

The enrollment selection logic properly filters by the current academic year and handles the case where no matching enrollment exists with a graceful catch block.


163-233: Good fallback handling.

The conditional rendering properly displays enrollment data when available and falls back to existing profile data when no matching enrollment is found, ensuring the UI remains functional in all scenarios.

lib/features/settings/presentation/pages/settings_page.dart (1)

118-149: LGTM!

The year selection UI integration properly displays the current selection and reloads the state after navigation returns, ensuring the display stays synchronized.

lib/l10n/app_localizations_ar.dart (1)

551-565: LGTM!

The new localization getters are properly implemented and consistent with the existing pattern in the generated file.

lib/features/profile/data/repositories/student_repository_impl.dart (2)

13-17: LGTM - Constructor with optional dependency injection.

The constructor pattern allows for proper dependency injection while providing sensible defaults.


35-42: LGTM - Manual year selection takes priority.

The logic correctly prioritizes a manually selected year over automatic detection, enabling the year selection feature.

lib/l10n/app_localizations.dart (1)

1101-1130: LGTM - New localization getters for year selection.

The five new getters (selectYearDescription, pleaseSelectYear, noEnrollmentsFound, errorLoadingData, notSelected) are properly defined with documentation and align with the year selection feature requirements.

lib/features/profile/presentation/bloc/profile_bloc.dart (2)

186-197: LGTM - Year-scoped cache storage.

The cache now correctly stores profile data keyed by the academic year ID, enabling proper cache separation for different years.


209-250: LGTM - Robust error handling with cache fallback.

The nested try-catch properly handles the case where fetching the academic year might also fail during error recovery. The fallback to cached data provides good offline resilience.

lib/config/routes/app_router.dart (1)

108-112: LGTM - Year selection route definition.

The new route is correctly defined outside the ShellRoute, ensuring users must complete year selection before accessing the main navigation shell.

lib/l10n/app_en.arb (1)

719-739: LGTM - New localization entries for year selection.

The five new ARB keys are properly structured with descriptions and provide appropriate English translations for the year selection feature.

lib/l10n/app_localizations_en.dart (1)

556-570: LGTM! Clean localization additions.

The new localization strings are well-formed, grammatically correct, and follow the existing pattern in the file. They appropriately support the year selection feature.

lib/features/profile/data/services/profile_cache_service.dart (2)

7-62: Well-structured year-scoped caching implementation.

The refactoring to year-scoped cache keys is clean and well-implemented. The approach of using key prefixes with yearId suffixes provides good namespace isolation, and storing _currentYearKey (line 27) enables tracking the most recently cached year. Error handling and null safety are properly maintained throughout.

Note: This is a breaking change to the cache structure—existing cached profile data (from pre-year-scoped versions) will become inaccessible after upgrade. Since this is cache data (ephemeral by nature), this is acceptable, though users will experience a brief reload after upgrading.


3-3: Version 2.5.3 is current and has no known security advisories.

The shared_preferences package version 2.5.3 is the latest release. No known CVEs or active security advisories were found for this package version.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/config/routes/app_router.dart (1)

63-81: Add redirect logic to enforce academic year selection after login.

The PR includes a YearSelectionService and route, but the redirect function in app_router.dart (lines 63-81) does not enforce year selection after successful authentication. After AuthSuccess, users should be redirected to /year-selection if they haven't selected an academic year (checked via YearSelectionService.getSelectedYearId()), ensuring they complete this step before accessing protected routes like the dashboard.

♻️ Duplicate comments (2)
lib/features/settings/presentation/pages/year_selection_page.dart (2)

18-22: Direct instantiation still breaks dependency injection.

The issue flagged in previous reviews remains: creating new instances of YearSelectionService, ProfileCacheService, and EnrollmentRepositoryImpl with ApiClient() bypasses dependency injection, which can cause problems with shared state, authentication context, and testability.


66-86: App restart still lacks user confirmation.

The issue flagged in previous reviews remains: the app restarts immediately after saving the year selection without asking for user confirmation, which can surprise users and cause them to lose unsaved context.

🧹 Nitpick comments (2)
lib/features/profile/data/models/academic_year.dart (1)

5-5: Consider making the constructor const for immutable instances.

Since all fields are final and there are no side effects, the constructor can be marked as const to enable compile-time constant instantiation and improve performance.

🔎 Suggested enhancement
-AcademicYear({required this.id, required this.code});
+const AcademicYear({required this.id, required this.code});

Additionally, consider overriding ==, hashCode, and toString for better debugging and object comparison (useful when storing in Sets or using as Map keys):

@override
bool operator ==(Object other) =>
    identical(this, other) ||
    other is AcademicYear &&
        runtimeType == other.runtimeType &&
        id == other.id &&
        code == other.code;

@override
int get hashCode => id.hashCode ^ code.hashCode;

@override
String toString() => 'AcademicYear(id: $id, code: $code)';
lib/features/profile/data/repositories/student_repository_impl.dart (1)

33-97: Consider extracting helper methods for improved readability.

The getCurrentAcademicYear method now handles two distinct flows (manual selection and automatic determination) across ~60 lines. While functionally correct, extracting helper methods could improve maintainability.

Potential extractions:

  • _getManuallySelectedYear() for lines 35-43
  • _computeMaxEnrollmentYear(enrollments) for lines 64-77
  • _determineAndPersistYear(currentYear, enrollments) for the fallback logic

This would make the main method read more like a high-level workflow.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6a50cd and be4ea63.

📒 Files selected for processing (6)
  • lib/config/routes/app_router.dart (3 hunks)
  • lib/features/profile/data/models/academic_year.dart (1 hunks)
  • lib/features/profile/data/models/student_detailed_info.dart (1 hunks)
  • lib/features/profile/data/repositories/student_repository_impl.dart (2 hunks)
  • lib/features/settings/presentation/pages/settings_page.dart (2 hunks)
  • lib/features/settings/presentation/pages/year_selection_page.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/features/profile/data/models/student_detailed_info.dart
  • lib/features/settings/presentation/pages/settings_page.dart
🔇 Additional comments (10)
lib/features/profile/data/models/academic_year.dart (1)

11-13: LGTM! The copyWith bug has been fixed.

The method now correctly preserves existing values when parameters are not provided. The nullable parameters and null-coalescing operators ensure that calling copyWith(id: newId) will keep the existing code value intact.

lib/features/settings/presentation/pages/year_selection_page.dart (2)

35-64: Well-implemented enrollment loading.

The method properly handles loading states, sorts enrollments by most recent first, pre-selects the current year if available, and includes comprehensive error handling with retry capability.


88-274: Solid UI implementation with good UX.

The build method comprehensively handles all states (loading, error, empty, success) with appropriate UI feedback. The responsive design with small-screen adaptations, proper localization, accessible radio buttons, and disabled confirm button when no selection is made all contribute to a polished user experience.

lib/config/routes/app_router.dart (3)

18-18: LGTM!

Import correctly added for the new YearSelectionPage.


25-25: LGTM!

Route name and path constants follow the existing naming conventions.

Also applies to: 43-43


93-97: Clarify route protection and layout intent.

The yearSelection route is outside the ShellRoute, which is by design:

  • It won't have the MainShell wrapper (navigation bars, persistent UI)
  • However, it IS protected by authentication via the global redirect function in GoRouter (lines 63-81), which redirects unauthenticated users to login for all routes except splash and login

Questions to verify:

  • Is the full-screen layout (without MainShell) intentional for a better UX during year selection?
  • If year selection is meant for post-login configuration, the current protection is correct. If it's for pre-login enrollment/discovery, you may need to exclude it from the auth redirect.
lib/features/profile/data/repositories/student_repository_impl.dart (4)

2-3: LGTM! Clean dependency injection.

The addition of YearSelectionService as a dependency is well-structured. The constructor follows the existing pattern with nullable parameters and sensible defaults, maintaining backward compatibility.

Also applies to: 11-11, 13-17


45-62: LGTM! Automatic flow is well-structured.

The logic to fetch UUID, enrollments, and current academic year is clear and includes appropriate null checks with informative error messages.


64-77: Nice fix! Empty enrollments edge case is now handled.

The guard at lines 68-70 properly addresses the concern raised in the previous review. By returning currentAcademicYear when enrollments are empty, the code avoids creating an invalid year with id=0.


79-93: LGTM! Fallback and persistence logic is sound.

The logic correctly handles graduated students by falling back to the maximum enrollment year when the current academic year exceeds it. Persisting the determined year via YearSelectionService ensures consistent behavior across subsequent calls.

@AliAkrem AliAkrem merged commit 923b4de into master Dec 20, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants