Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ So I decided to rebuild it from scratch, with a better design and offline suppor
<img src="screenshot/assessment.jpg" width="200" alt="Assessment Screen" />
<img src="screenshot/exams.jpg" width="200" alt="Exams Screen" />
<img src="screenshot/timeline.jpg" width="200" alt="Timeline Screen" />
<img src="screenshot/discharge.jpg" width="200" alt="Discharge Screen" />
</p>

## App Modules
Expand All @@ -42,6 +43,8 @@ So I decided to rebuild it from scratch, with a better design and offline suppor
- **Performance Tracking**
- **Academic history**
- **Weekly Timeline**
- **Student Discharge**



## Technical Details
Expand Down
2 changes: 2 additions & 0 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:progres/features/profile/presentation/bloc/profile_bloc.dart';
import 'package:progres/features/subject/presentation/bloc/subject_bloc.dart';
import 'package:progres/features/timeline/presentation/blocs/timeline_bloc.dart';
import 'package:progres/features/transcript/presentation/bloc/transcript_bloc.dart';
import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart';
import 'package:flutter_gen/gen_l10n/gallery_localizations.dart';

class ProgresApp extends StatelessWidget {
Expand All @@ -32,6 +33,7 @@ class ProgresApp extends StatelessWidget {
BlocProvider(create: (context) => injector<SubjectBloc>()),
BlocProvider(create: (context) => injector<TranscriptBloc>()),
BlocProvider(create: (context) => injector<EnrollmentBloc>()),
BlocProvider(create: (context) => injector<StudentDischargeBloc>()),
],
child: CalendarControllerProvider(
controller: EventController(),
Expand Down
8 changes: 8 additions & 0 deletions lib/config/routes/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:progres/features/academics/presentation/pages/academic_performance_page.dart';
import 'package:progres/features/groups/presentation/pages/groups_page.dart';
import 'package:progres/features/discharge/presentation/pages/discharge_page.dart';
import 'package:progres/features/subject/presentation/pages/subject_page.dart';
import 'package:progres/features/timeline/presentation/pages/timeline_page.dart';
import 'package:progres/features/transcript/presentation/pages/transcript_page.dart';
Expand All @@ -27,6 +28,7 @@ class AppRouter {
static const String enrollments = 'enrollments';
static const String timeline = 'timeline';
static const String transcripts = 'transcripts';
static const String discharge = 'discharge';
static const String about = 'about';

// Route paths
Expand All @@ -41,6 +43,7 @@ class AppRouter {
static const String enrollmentsPath = 'enrollments';
static const String timelinePath = 'timeline';
static const String transcriptsPath = 'transcripts';
static const String dischargePath = 'discharge';
static const String aboutPath = 'about';

late final GoRouter router;
Expand Down Expand Up @@ -108,6 +111,11 @@ class AppRouter {
name: transcripts,
builder: (context, state) => const TranscriptPage(),
),
GoRoute(
path: dischargePath,
name: discharge,
builder: (context, state) => const DischargePage(),
),
],
),
GoRoute(
Expand Down
6 changes: 6 additions & 0 deletions lib/core/di/injector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import 'package:progres/features/timeline/presentation/blocs/timeline_bloc.dart'
import 'package:progres/features/transcript/data/repositories/transcript_repository_impl.dart';
import 'package:progres/features/transcript/data/services/transcript_cache_service.dart';
import 'package:progres/features/transcript/presentation/bloc/transcript_bloc.dart';
import 'package:progres/features/discharge/data/repository/discharge_repository_impl.dart';
import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart';

final injector = GetIt.instance;

Expand Down Expand Up @@ -53,6 +55,7 @@ Future<void> initDependencies() async {
injector.registerLazySingleton(
() => AcademicPerformencetRepositoryImpl(apiClient: injector()),
);
injector.registerLazySingleton(() => StudentDischargeRepositoryImpl());
injector.registerLazySingleton(() => TimelineCacheService());
injector.registerLazySingleton(() => EnrollmentCacheService());
injector.registerLazySingleton(() => TranscriptCacheService());
Expand Down Expand Up @@ -105,4 +108,7 @@ Future<void> initDependencies() async {
cacheService: injector(),
),
);
injector.registerFactory(
() => StudentDischargeBloc(studentDischargeRepository: injector()),
);
}
4 changes: 4 additions & 0 deletions lib/core/network/api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class ApiClient {
},
),
);
_initializeCacheManager();
}
Comment on lines +40 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Race condition in cache manager initialization

The asynchronous cache manager initialization in the constructor creates a race condition. Since _initializeCacheManager() is not awaited, the constructor completes before _cacheManager is initialized, potentially causing runtime exceptions when methods like get() are called immediately after object construction.

Consider these solutions:

Option 1: Factory constructor pattern

- ApiClient({FlutterSecureStorage? secureStorage})
-   : _secureStorage = secureStorage ?? const FlutterSecureStorage() {
+ ApiClient._({FlutterSecureStorage? secureStorage})
+   : _secureStorage = secureStorage ?? const FlutterSecureStorage() {
   _dio = Dio(/* ... */);
   // ... other initialization
-   _initializeCacheManager();
+ }
+
+ static Future<ApiClient> create({FlutterSecureStorage? secureStorage}) async {
+   final client = ApiClient._(secureStorage: secureStorage);
+   await client._initializeCacheManager();
+   return client;
 }

Option 2: Make initialization method async and handle properly

- void _initializeCacheManager() {
+ Future<void> _initializeCacheManager() async {
-   CacheManager.getInstance().then((value) => _cacheManager = value);
+   _cacheManager = await CacheManager.getInstance();
 }
🤖 Prompt for AI Agents
In lib/core/network/api_client.dart around lines 40 to 41, the call to
_initializeCacheManager() is asynchronous but not awaited, causing a race
condition where _cacheManager may be uninitialized when used. To fix this,
refactor the class to use a factory constructor that performs async
initialization and returns a fully initialized instance, or make the
initialization method async and ensure callers await the completion before using
the instance. This guarantees _cacheManager is ready before any method calls.


void _initializeCacheManager() {
CacheManager.getInstance().then((value) => _cacheManager = value);
}
Comment on lines +43 to 45
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for cache manager initialization

The cache manager initialization lacks error handling, which could leave _cacheManager uninitialized if CacheManager.getInstance() fails.

- void _initializeCacheManager() {
-   CacheManager.getInstance().then((value) => _cacheManager = value);
+ Future<void> _initializeCacheManager() async {
+   try {
+     _cacheManager = await CacheManager.getInstance();
+   } catch (e) {
+     debugPrint('Failed to initialize cache manager: $e');
+     // Consider providing a fallback or rethrowing
+     rethrow;
+   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void _initializeCacheManager() {
CacheManager.getInstance().then((value) => _cacheManager = value);
}
Future<void> _initializeCacheManager() async {
try {
_cacheManager = await CacheManager.getInstance();
} catch (e) {
debugPrint('Failed to initialize cache manager: $e');
// Consider providing a fallback or rethrowing
rethrow;
}
}
🤖 Prompt for AI Agents
In lib/core/network/api_client.dart around lines 43 to 45, the
_initializeCacheManager method calls CacheManager.getInstance() without handling
potential errors. Modify the method to include error handling for the
asynchronous call, such as using a try-catch block or handling errors in the
then callback, to ensure that any failure in obtaining the cache manager is
caught and handled appropriately, preventing _cacheManager from remaining
uninitialized silently.


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AcademicsCacheService {
Expand All @@ -20,7 +21,7 @@ class AcademicsCacheService {
);
return true;
} catch (e) {
print('Error caching academics data: $e');
debugPrint('Error caching academics data: $e');
return false;
}
}
Expand All @@ -35,7 +36,7 @@ class AcademicsCacheService {

return jsonDecode(dataString) as List<dynamic>;
} catch (e) {
print('Error retrieving cached academics data: $e');
debugPrint('Error retrieving cached academics data: $e');
return null;
}
}
Expand All @@ -51,7 +52,7 @@ class AcademicsCacheService {

return DateTime.parse(timestamp);
} catch (e) {
print('Error getting last updated time: $e');
debugPrint('Error getting last updated time: $e');
return null;
}
}
Expand All @@ -70,7 +71,7 @@ class AcademicsCacheService {
}
return true;
} catch (e) {
print('Error clearing academics cache: $e');
debugPrint('Error clearing academics cache: $e');
return false;
}
}
Expand Down
12 changes: 6 additions & 6 deletions lib/features/auth/presentation/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,35 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
try {
event.context?.read<TranscriptBloc>().add(const ClearTranscriptCache());
} catch (e) {
print('Note: Could not clear transcript cache. ${e.toString()}');
debugPrint('Note: Could not clear transcript cache. ${e.toString()}');
}
try {
event.context?.read<TimelineBloc>().add(ClearTimelineCache());
} catch (e) {
print('Note: Could not clear timeline cache. ${e.toString()}');
debugPrint('Note: Could not clear timeline cache. ${e.toString()}');
}

try {
event.context?.read<EnrollmentBloc>().add(ClearEnrollmentsCache());
} catch (e) {
print('Note: Could not clear enrollment cache. ${e.toString()}');
debugPrint('Note: Could not clear enrollment cache. ${e.toString()}');
}

try {
event.context?.read<StudentGroupsBloc>().add(ClearGroupsCache());
} catch (e) {
print('Note: Could not clear groups cache. ${e.toString()}');
debugPrint('Note: Could not clear groups cache. ${e.toString()}');
}

try {
event.context?.read<SubjectBloc>().add(ClearSubjectCache());
} catch (e) {
print('Note: Could not clear subject cache. ${e.toString()}');
debugPrint('Note: Could not clear subject cache. ${e.toString()}');
}
try {
event.context?.read<ProfileBloc>().add(ClearProfileCacheEvent());
} catch (e) {
print('Note: Could not clear profile cache. ${e.toString()}');
debugPrint('Note: Could not clear profile cache. ${e.toString()}');
}

emit(AuthLoggedOut());
Expand Down
7 changes: 7 additions & 0 deletions lib/features/dashboard/presentation/widgets/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ Widget buildDashboard(ProfileLoaded state, BuildContext context) {
color: AppTheme.AppPrimary,
onTap: () => context.goNamed(AppRouter.transcripts),
),
buildGridCard(
context,
title: GalleryLocalizations.of(context)!.myDischarge,
icon: Icons.assignment_turned_in_outlined,
color: AppTheme.AppPrimary,
onTap: () => context.goNamed(AppRouter.discharge),
),
],
),

Expand Down
35 changes: 35 additions & 0 deletions lib/features/discharge/data/models/discharge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class StudentDischarge {
final bool sitDep;
final bool sitBf;
final bool sitBc;
final bool sitRu;
final bool sitBrs;

StudentDischarge({
this.sitDep = false,
this.sitBf = false,
this.sitBc = false,
this.sitRu = false,
this.sitBrs = false,
});

factory StudentDischarge.fromJson(Map<String, dynamic> json) {
return StudentDischarge(
sitBc: (json['sitBc'] as int?) == 1,
sitBrs: (json['sitBrs'] as int?) == 1,
sitDep: (json['sitDep'] as int?) == 1,
sitBf: (json['sitBf'] as int?) == 1,
sitRu: (json['sitRu'] as int?) == 1,
);
}

Map<String, dynamic> toJson() {
return {
'sitDep': sitDep,
'sitBf': sitBf,
'sitBc': sitBc,
'sitRu': sitRu,
'sitBrs': sitBrs,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:progres/features/discharge/data/models/discharge.dart';
import 'package:progres/features/discharge/data/services/discharge_api_client.dart';

class StudentDischargeRepositoryImpl {
final DischargeApiClient _apiClient;

StudentDischargeRepositoryImpl({DischargeApiClient? apiClient})
: _apiClient = apiClient ?? DischargeApiClient();

Future<StudentDischarge> getStudentDischarge() async {
try {
final uuid = await _apiClient.getUuid();
final response = await _apiClient.get('/$uuid/qitus');

final List<dynamic> dischargeJson = response.data;

if (dischargeJson.isEmpty) {
throw DischargeNotRequiredException(
'Discharge is not required for this student',
);
}

return StudentDischarge.fromJson(dischargeJson[0]);
} catch (e) {
rethrow;
}
}
}

class DischargeNotRequiredException implements Exception {
final String message;
DischargeNotRequiredException(this.message);

@override
String toString() => message;
}
Loading
Loading