Skip to content

Commit 7c4c420

Browse files
committed
feat: allow student to review his discharge
1 parent e8e9727 commit 7c4c420

File tree

14 files changed

+1062
-2
lines changed

14 files changed

+1062
-2
lines changed

lib/app.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:progres/features/profile/presentation/bloc/profile_bloc.dart';
1414
import 'package:progres/features/subject/presentation/bloc/subject_bloc.dart';
1515
import 'package:progres/features/timeline/presentation/blocs/timeline_bloc.dart';
1616
import 'package:progres/features/transcript/presentation/bloc/transcript_bloc.dart';
17+
import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart';
1718
import 'package:flutter_gen/gen_l10n/gallery_localizations.dart';
1819

1920
class ProgresApp extends StatelessWidget {
@@ -32,6 +33,7 @@ class ProgresApp extends StatelessWidget {
3233
BlocProvider(create: (context) => injector<SubjectBloc>()),
3334
BlocProvider(create: (context) => injector<TranscriptBloc>()),
3435
BlocProvider(create: (context) => injector<EnrollmentBloc>()),
36+
BlocProvider(create: (context) => injector<StudentDischargeBloc>()),
3537
],
3638
child: CalendarControllerProvider(
3739
controller: EventController(),

lib/config/routes/app_router.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:go_router/go_router.dart';
44
import 'package:progres/features/academics/presentation/pages/academic_performance_page.dart';
55
import 'package:progres/features/groups/presentation/pages/groups_page.dart';
6+
import 'package:progres/features/discharge/presentation/pages/discharge_page.dart';
67
import 'package:progres/features/subject/presentation/pages/subject_page.dart';
78
import 'package:progres/features/timeline/presentation/pages/timeline_page.dart';
89
import 'package:progres/features/transcript/presentation/pages/transcript_page.dart';
@@ -27,6 +28,7 @@ class AppRouter {
2728
static const String enrollments = 'enrollments';
2829
static const String timeline = 'timeline';
2930
static const String transcripts = 'transcripts';
31+
static const String discharge = 'discharge';
3032
static const String about = 'about';
3133

3234
// Route paths
@@ -41,6 +43,7 @@ class AppRouter {
4143
static const String enrollmentsPath = 'enrollments';
4244
static const String timelinePath = 'timeline';
4345
static const String transcriptsPath = 'transcripts';
46+
static const String dischargePath = 'discharge';
4447
static const String aboutPath = 'about';
4548

4649
late final GoRouter router;
@@ -108,6 +111,11 @@ class AppRouter {
108111
name: transcripts,
109112
builder: (context, state) => const TranscriptPage(),
110113
),
114+
GoRoute(
115+
path: dischargePath,
116+
name: discharge,
117+
builder: (context, state) => const DischargePage(),
118+
),
111119
],
112120
),
113121
GoRoute(

lib/core/di/injector.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import 'package:progres/features/timeline/presentation/blocs/timeline_bloc.dart'
2424
import 'package:progres/features/transcript/data/repositories/transcript_repository_impl.dart';
2525
import 'package:progres/features/transcript/data/services/transcript_cache_service.dart';
2626
import 'package:progres/features/transcript/presentation/bloc/transcript_bloc.dart';
27+
import 'package:progres/features/discharge/data/repository/discharge_repository_impl.dart';
28+
import 'package:progres/features/discharge/data/services/discharge_cache_service.dart';
29+
import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart';
2730

2831
final injector = GetIt.instance;
2932

@@ -53,11 +56,13 @@ Future<void> initDependencies() async {
5356
injector.registerLazySingleton(
5457
() => AcademicPerformencetRepositoryImpl(apiClient: injector()),
5558
);
59+
injector.registerLazySingleton(() => StudentDischargeRepositoryImpl());
5660
injector.registerLazySingleton(() => TimelineCacheService());
5761
injector.registerLazySingleton(() => EnrollmentCacheService());
5862
injector.registerLazySingleton(() => TranscriptCacheService());
5963
injector.registerLazySingleton(() => GroupsCacheService());
6064
injector.registerLazySingleton(() => SubjectCacheService());
65+
injector.registerLazySingleton(() => DischargeCacheService());
6166

6267
// Register BLoCs
6368
injector.registerFactory(() => ThemeBloc()..add(LoadTheme()));
@@ -105,4 +110,10 @@ Future<void> initDependencies() async {
105110
cacheService: injector(),
106111
),
107112
);
113+
injector.registerFactory(
114+
() => StudentDischargeBloc(
115+
studentDischargeRepository: injector(),
116+
cacheService: injector(),
117+
),
118+
);
108119
}

lib/features/dashboard/presentation/widgets/dashboard.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ Widget buildDashboard(ProfileLoaded state, BuildContext context) {
136136
color: AppTheme.AppPrimary,
137137
onTap: () => context.goNamed(AppRouter.transcripts),
138138
),
139+
buildGridCard(
140+
context,
141+
title: GalleryLocalizations.of(context)!.myDischarge,
142+
icon: Icons.assignment_turned_in_outlined,
143+
color: AppTheme.AppPrimary,
144+
onTap: () => context.goNamed(AppRouter.discharge),
145+
),
139146
],
140147
),
141148

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class StudentDischarge {
2+
final bool sitDep;
3+
final bool sitBf;
4+
final bool sitBc;
5+
final bool sitRu;
6+
final bool sitBr;
7+
8+
StudentDischarge({
9+
this.sitDep = false,
10+
this.sitBf = false,
11+
this.sitBc = false,
12+
this.sitRu = false,
13+
this.sitBr = false,
14+
});
15+
16+
factory StudentDischarge.fromJson(Map<String, dynamic> json) {
17+
return StudentDischarge(
18+
sitBc: (json['sitBc'] as int?) == 1,
19+
sitBr:
20+
(json['sitBrs'] as int?) == 1, // Note: API uses 'sitBrs' not 'sitBr'
21+
sitDep: (json['sitDep'] as int?) == 1,
22+
sitBf: (json['sitBf'] as int?) == 1,
23+
sitRu: (json['sitRu'] as int?) == 1,
24+
);
25+
}
26+
27+
Map<String, dynamic> toJson() {
28+
return {
29+
'sitDep': sitDep,
30+
'sitBf': sitBf,
31+
'sitBc': sitBc,
32+
'sitRu': sitRu,
33+
'sitBr': sitBr,
34+
};
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:progres/features/discharge/data/models/dischage.dart';
2+
import 'package:progres/features/discharge/data/services/discharge_api_client.dart';
3+
4+
class StudentDischargeRepositoryImpl {
5+
final DischargeApiClient _apiClient;
6+
7+
StudentDischargeRepositoryImpl({DischargeApiClient? apiClient})
8+
: _apiClient = apiClient ?? DischargeApiClient();
9+
10+
Future<StudentDischarge> getStudentDischarge() async {
11+
try {
12+
final uuid = await _apiClient.getUuid();
13+
final response = await _apiClient.get('/$uuid/qitus');
14+
15+
final List<dynamic> dischargeJson = response.data;
16+
17+
if (dischargeJson.isEmpty) {
18+
throw DischargeNotRequiredException(
19+
'Discharge is not required for this student',
20+
);
21+
}
22+
23+
return StudentDischarge.fromJson(dischargeJson[0]);
24+
} catch (e) {
25+
rethrow;
26+
}
27+
}
28+
}
29+
30+
class DischargeNotRequiredException implements Exception {
31+
final String message;
32+
DischargeNotRequiredException(this.message);
33+
34+
@override
35+
String toString() => message;
36+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'dart:async';
2+
import 'package:dio/dio.dart';
3+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4+
import 'package:connectivity_plus/connectivity_plus.dart';
5+
import 'package:progres/core/network/cache_manager.dart';
6+
7+
class DischargeApiClient {
8+
static const String baseUrl = 'https://quittance.mesrs.dz/api';
9+
10+
late final Dio _dio;
11+
final FlutterSecureStorage _secureStorage;
12+
late final CacheManager _cacheManager;
13+
final Duration _shortTimeout = const Duration(seconds: 5);
14+
final Connectivity _connectivity = Connectivity();
15+
16+
DischargeApiClient({FlutterSecureStorage? secureStorage})
17+
: _secureStorage = secureStorage ?? const FlutterSecureStorage() {
18+
_dio = Dio(
19+
BaseOptions(
20+
baseUrl: baseUrl,
21+
connectTimeout: const Duration(seconds: 30),
22+
receiveTimeout: const Duration(seconds: 30),
23+
headers: {'Content-Type': 'application/json'},
24+
),
25+
);
26+
_dio.interceptors.add(
27+
InterceptorsWrapper(
28+
onRequest: (options, handler) async {
29+
final token = await _secureStorage.read(key: 'auth_token');
30+
if (token != null) {
31+
options.headers['authorization'] = token;
32+
}
33+
return handler.next(options);
34+
},
35+
onError: (error, handler) {
36+
// Handle errors
37+
return handler.next(error);
38+
},
39+
),
40+
);
41+
CacheManager.getInstance().then((value) => _cacheManager = value);
42+
}
43+
44+
Future<bool> get isConnected async {
45+
final result = await _connectivity.checkConnectivity();
46+
return result != ConnectivityResult.none;
47+
}
48+
49+
Future<void> saveToken(String token) async {
50+
await _secureStorage.write(key: 'auth_token', value: token);
51+
}
52+
53+
Future<void> saveUuid(String uuid) async {
54+
await _secureStorage.write(key: 'uuid', value: uuid);
55+
}
56+
57+
Future<void> saveEtablissementId(String etablissementId) async {
58+
await _secureStorage.write(key: 'etablissement_id', value: etablissementId);
59+
}
60+
61+
Future<String?> getUuid() async {
62+
return await _secureStorage.read(key: 'uuid');
63+
}
64+
65+
Future<String?> getEtablissementId() async {
66+
return await _secureStorage.read(key: 'etablissement_id');
67+
}
68+
69+
Future<bool> isLoggedIn() async {
70+
final token = await _secureStorage.read(key: 'auth_token');
71+
return token != null;
72+
}
73+
74+
// Generate a cache key string based on path and query parameters
75+
String _cacheKey(String path, Map<String, dynamic>? queryParameters) {
76+
final queryStr =
77+
queryParameters != null
78+
? Uri(queryParameters: queryParameters).query
79+
: '';
80+
return '$path?$queryStr';
81+
}
82+
83+
Future<Response> get(
84+
String path, {
85+
Map<String, dynamic>? queryParameters,
86+
}) async {
87+
final key = _cacheKey(path, queryParameters);
88+
89+
if (!await isConnected) {
90+
// offline - use cached data if available
91+
final cachedData = _cacheManager.getCache(key);
92+
if (cachedData != null) {
93+
return Response(
94+
requestOptions: RequestOptions(path: path),
95+
data: cachedData,
96+
statusCode: 200,
97+
);
98+
} else {
99+
// No cache, throw offline error
100+
throw DioException(
101+
requestOptions: RequestOptions(path: path),
102+
error: 'No internet connection and no cached data',
103+
);
104+
}
105+
}
106+
107+
try {
108+
// Try to get fresh data with a short timeout for fast fallback on slow responses
109+
final response = await _dio.get(
110+
path,
111+
queryParameters: queryParameters,
112+
options: Options(
113+
sendTimeout: _shortTimeout,
114+
receiveTimeout: _shortTimeout,
115+
),
116+
);
117+
await _cacheManager.saveCache(key, response.data);
118+
return response;
119+
} catch (e) {
120+
// On failure, return cached data if available
121+
final cachedData = _cacheManager.getCache(key);
122+
if (cachedData != null) {
123+
return Response(
124+
requestOptions: RequestOptions(path: path),
125+
data: cachedData,
126+
statusCode: 200,
127+
);
128+
}
129+
rethrow;
130+
}
131+
}
132+
133+
Future<Response> post(String path, {dynamic data}) async {
134+
try {
135+
final response = await _dio.post(path, data: data);
136+
return response;
137+
} catch (e) {
138+
rethrow;
139+
}
140+
}
141+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'dart:convert';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:progres/features/discharge/data/models/dischage.dart';
4+
5+
class DischargeCacheService {
6+
// Keys for SharedPreferences
7+
static const String _dischargeKey = 'cached_discharge';
8+
static const String _lastUpdatedKeyPrefix = 'last_updated_';
9+
10+
// Save discharge to cache
11+
Future<bool> cacheDischarge(StudentDischarge discharge) async {
12+
try {
13+
final prefs = await SharedPreferences.getInstance();
14+
final dischargeJson = discharge.toJson();
15+
await prefs.setString(_dischargeKey, jsonEncode(dischargeJson));
16+
await prefs.setString(
17+
'${_lastUpdatedKeyPrefix}discharge',
18+
DateTime.now().toIso8601String(),
19+
);
20+
return true;
21+
} catch (e) {
22+
print('Error caching discharge: $e');
23+
return false;
24+
}
25+
}
26+
27+
// Retrieve discharge from cache
28+
Future<StudentDischarge?> getCachedDischarge() async {
29+
try {
30+
final prefs = await SharedPreferences.getInstance();
31+
final dischargeString = prefs.getString(_dischargeKey);
32+
33+
if (dischargeString == null) return null;
34+
35+
final Map<String, dynamic> decodedJson = jsonDecode(dischargeString);
36+
return StudentDischarge.fromJson(decodedJson);
37+
} catch (e) {
38+
print('Error retrieving cached discharge: $e');
39+
return null;
40+
}
41+
}
42+
43+
// Get last update timestamp for discharge data
44+
Future<DateTime?> getLastUpdated() async {
45+
try {
46+
final prefs = await SharedPreferences.getInstance();
47+
const key = '${_lastUpdatedKeyPrefix}discharge';
48+
49+
final timestamp = prefs.getString(key);
50+
if (timestamp == null) return null;
51+
52+
return DateTime.parse(timestamp);
53+
} catch (e) {
54+
print('Error getting last updated time: $e');
55+
return null;
56+
}
57+
}
58+
59+
// Clear discharge cache
60+
Future<bool> clearCache() async {
61+
try {
62+
final prefs = await SharedPreferences.getInstance();
63+
await prefs.remove(_dischargeKey);
64+
await prefs.remove('${_lastUpdatedKeyPrefix}discharge');
65+
return true;
66+
} catch (e) {
67+
print('Error clearing discharge cache: $e');
68+
return false;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)