Skip to content

Commit c4c0950

Browse files
authored
Merge pull request #6 from AliAkrem/feat/student-discharge
Feat/student discharge
2 parents 448b684 + 2116706 commit c4c0950

File tree

17 files changed

+972
-6
lines changed

17 files changed

+972
-6
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ So I decided to rebuild it from scratch, with a better design and offline suppor
3232
<img src="screenshot/assessment.jpg" width="200" alt="Assessment Screen" />
3333
<img src="screenshot/exams.jpg" width="200" alt="Exams Screen" />
3434
<img src="screenshot/timeline.jpg" width="200" alt="Timeline Screen" />
35+
<img src="screenshot/discharge.jpg" width="200" alt="Discharge Screen" />
3536
</p>
3637

3738
## App Modules
@@ -42,6 +43,8 @@ So I decided to rebuild it from scratch, with a better design and offline suppor
4243
- **Performance Tracking**
4344
- **Academic history**
4445
- **Weekly Timeline**
46+
- **Student Discharge**
47+
4548

4649

4750
## Technical Details

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ 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/presentation/bloc/discharge_bloc.dart';
2729

2830
final injector = GetIt.instance;
2931

@@ -53,6 +55,7 @@ Future<void> initDependencies() async {
5355
injector.registerLazySingleton(
5456
() => AcademicPerformencetRepositoryImpl(apiClient: injector()),
5557
);
58+
injector.registerLazySingleton(() => StudentDischargeRepositoryImpl());
5659
injector.registerLazySingleton(() => TimelineCacheService());
5760
injector.registerLazySingleton(() => EnrollmentCacheService());
5861
injector.registerLazySingleton(() => TranscriptCacheService());
@@ -105,4 +108,7 @@ Future<void> initDependencies() async {
105108
cacheService: injector(),
106109
),
107110
);
111+
injector.registerFactory(
112+
() => StudentDischargeBloc(studentDischargeRepository: injector()),
113+
);
108114
}

lib/core/network/api_client.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class ApiClient {
3737
},
3838
),
3939
);
40+
_initializeCacheManager();
41+
}
42+
43+
void _initializeCacheManager() {
4044
CacheManager.getInstance().then((value) => _cacheManager = value);
4145
}
4246

lib/features/academics/data/services/academics_cache_service.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'package:flutter/foundation.dart';
23
import 'package:shared_preferences/shared_preferences.dart';
34

45
class AcademicsCacheService {
@@ -20,7 +21,7 @@ class AcademicsCacheService {
2021
);
2122
return true;
2223
} catch (e) {
23-
print('Error caching academics data: $e');
24+
debugPrint('Error caching academics data: $e');
2425
return false;
2526
}
2627
}
@@ -35,7 +36,7 @@ class AcademicsCacheService {
3536

3637
return jsonDecode(dataString) as List<dynamic>;
3738
} catch (e) {
38-
print('Error retrieving cached academics data: $e');
39+
debugPrint('Error retrieving cached academics data: $e');
3940
return null;
4041
}
4142
}
@@ -51,7 +52,7 @@ class AcademicsCacheService {
5152

5253
return DateTime.parse(timestamp);
5354
} catch (e) {
54-
print('Error getting last updated time: $e');
55+
debugPrint('Error getting last updated time: $e');
5556
return null;
5657
}
5758
}
@@ -70,7 +71,7 @@ class AcademicsCacheService {
7071
}
7172
return true;
7273
} catch (e) {
73-
print('Error clearing academics cache: $e');
74+
debugPrint('Error clearing academics cache: $e');
7475
return false;
7576
}
7677
}

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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class StudentDischarge {
2+
final bool sitDep;
3+
final bool sitBf;
4+
final bool sitBc;
5+
final bool sitRu;
6+
final bool sitBrs;
7+
8+
StudentDischarge({
9+
this.sitDep = false,
10+
this.sitBf = false,
11+
this.sitBc = false,
12+
this.sitRu = false,
13+
this.sitBrs = false,
14+
});
15+
16+
factory StudentDischarge.fromJson(Map<String, dynamic> json) {
17+
return StudentDischarge(
18+
sitBc: (json['sitBc'] as int?) == 1,
19+
sitBrs: (json['sitBrs'] as int?) == 1,
20+
sitDep: (json['sitDep'] as int?) == 1,
21+
sitBf: (json['sitBf'] as int?) == 1,
22+
sitRu: (json['sitRu'] as int?) == 1,
23+
);
24+
}
25+
26+
Map<String, dynamic> toJson() {
27+
return {
28+
'sitDep': sitDep,
29+
'sitBf': sitBf,
30+
'sitBc': sitBc,
31+
'sitRu': sitRu,
32+
'sitBrs': sitBrs,
33+
};
34+
}
35+
}
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/discharge.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+
}

0 commit comments

Comments
 (0)