Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
11 changes: 11 additions & 0 deletions lib/core/di/injector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ 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/data/services/discharge_cache_service.dart';
import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart';

final injector = GetIt.instance;

Expand Down Expand Up @@ -53,11 +56,13 @@ Future<void> initDependencies() async {
injector.registerLazySingleton(
() => AcademicPerformencetRepositoryImpl(apiClient: injector()),
);
injector.registerLazySingleton(() => StudentDischargeRepositoryImpl());
injector.registerLazySingleton(() => TimelineCacheService());
injector.registerLazySingleton(() => EnrollmentCacheService());
injector.registerLazySingleton(() => TranscriptCacheService());
injector.registerLazySingleton(() => GroupsCacheService());
injector.registerLazySingleton(() => SubjectCacheService());
injector.registerLazySingleton(() => DischargeCacheService());

// Register BLoCs
injector.registerFactory(() => ThemeBloc()..add(LoadTheme()));
Expand Down Expand Up @@ -105,4 +110,10 @@ Future<void> initDependencies() async {
cacheService: injector(),
),
);
injector.registerFactory(
() => StudentDischargeBloc(
studentDischargeRepository: injector(),
cacheService: injector(),
),
);
}
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
36 changes: 36 additions & 0 deletions lib/features/discharge/data/models/dischage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class StudentDischarge {
final bool sitDep;
final bool sitBf;
final bool sitBc;
final bool sitRu;
final bool sitBr;

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

factory StudentDischarge.fromJson(Map<String, dynamic> json) {
return StudentDischarge(
sitBc: (json['sitBc'] as int?) == 1,
sitBr:
(json['sitBrs'] as int?) == 1, // Note: API uses 'sitBrs' not 'sitBr'
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,
'sitBr': sitBr,
};
}
}
Comment thread
AliAkrem marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:progres/features/discharge/data/models/dischage.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;
}
141 changes: 141 additions & 0 deletions lib/features/discharge/data/services/discharge_api_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:progres/core/network/cache_manager.dart';

class DischargeApiClient {
static const String baseUrl = 'https://quittance.mesrs.dz/api';
Comment thread
AliAkrem marked this conversation as resolved.

late final Dio _dio;
final FlutterSecureStorage _secureStorage;
late final CacheManager _cacheManager;
final Duration _shortTimeout = const Duration(seconds: 5);
final Connectivity _connectivity = Connectivity();

DischargeApiClient({FlutterSecureStorage? secureStorage})
: _secureStorage = secureStorage ?? const FlutterSecureStorage() {
_dio = Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {'Content-Type': 'application/json'},
),
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _secureStorage.read(key: 'auth_token');
if (token != null) {
options.headers['authorization'] = token;
}
return handler.next(options);
},
onError: (error, handler) {
// Handle errors
return handler.next(error);
},
),
);
CacheManager.getInstance().then((value) => _cacheManager = value);
Comment thread
AliAkrem marked this conversation as resolved.
}

Future<bool> get isConnected async {
final result = await _connectivity.checkConnectivity();
return result != ConnectivityResult.none;
}

Future<void> saveToken(String token) async {
await _secureStorage.write(key: 'auth_token', value: token);
}

Future<void> saveUuid(String uuid) async {
await _secureStorage.write(key: 'uuid', value: uuid);
}

Future<void> saveEtablissementId(String etablissementId) async {
await _secureStorage.write(key: 'etablissement_id', value: etablissementId);
}

Future<String?> getUuid() async {
return await _secureStorage.read(key: 'uuid');
}

Future<String?> getEtablissementId() async {
return await _secureStorage.read(key: 'etablissement_id');
}

Future<bool> isLoggedIn() async {
final token = await _secureStorage.read(key: 'auth_token');
return token != null;
}

// Generate a cache key string based on path and query parameters
String _cacheKey(String path, Map<String, dynamic>? queryParameters) {
final queryStr =
queryParameters != null
? Uri(queryParameters: queryParameters).query
: '';
return '$path?$queryStr';
}

Future<Response> get(
String path, {
Map<String, dynamic>? queryParameters,
}) async {
final key = _cacheKey(path, queryParameters);

if (!await isConnected) {
// offline - use cached data if available
final cachedData = _cacheManager.getCache(key);
if (cachedData != null) {
return Response(
requestOptions: RequestOptions(path: path),
data: cachedData,
statusCode: 200,
);
} else {
// No cache, throw offline error
throw DioException(
requestOptions: RequestOptions(path: path),
error: 'No internet connection and no cached data',
);
}
}

try {
// Try to get fresh data with a short timeout for fast fallback on slow responses
final response = await _dio.get(
path,
queryParameters: queryParameters,
options: Options(
sendTimeout: _shortTimeout,
receiveTimeout: _shortTimeout,
),
);
await _cacheManager.saveCache(key, response.data);
return response;
} catch (e) {
// On failure, return cached data if available
final cachedData = _cacheManager.getCache(key);
if (cachedData != null) {
return Response(
requestOptions: RequestOptions(path: path),
data: cachedData,
statusCode: 200,
);
}
rethrow;
}
}

Future<Response> post(String path, {dynamic data}) async {
try {
final response = await _dio.post(path, data: data);
return response;
} catch (e) {
rethrow;
}
}
}
71 changes: 71 additions & 0 deletions lib/features/discharge/data/services/discharge_cache_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:progres/features/discharge/data/models/dischage.dart';

class DischargeCacheService {
// Keys for SharedPreferences
static const String _dischargeKey = 'cached_discharge';
static const String _lastUpdatedKeyPrefix = 'last_updated_';

// Save discharge to cache
Future<bool> cacheDischarge(StudentDischarge discharge) async {
try {
final prefs = await SharedPreferences.getInstance();
final dischargeJson = discharge.toJson();
await prefs.setString(_dischargeKey, jsonEncode(dischargeJson));
await prefs.setString(
'${_lastUpdatedKeyPrefix}discharge',
DateTime.now().toIso8601String(),
);
return true;
} catch (e) {
print('Error caching discharge: $e');
Comment thread
AliAkrem marked this conversation as resolved.
Outdated
return false;
}
}

// Retrieve discharge from cache
Future<StudentDischarge?> getCachedDischarge() async {
try {
final prefs = await SharedPreferences.getInstance();
final dischargeString = prefs.getString(_dischargeKey);

if (dischargeString == null) return null;

final Map<String, dynamic> decodedJson = jsonDecode(dischargeString);
return StudentDischarge.fromJson(decodedJson);
} catch (e) {
print('Error retrieving cached discharge: $e');
return null;
}
Comment thread
AliAkrem marked this conversation as resolved.
Outdated
}

// Get last update timestamp for discharge data
Future<DateTime?> getLastUpdated() async {
try {
final prefs = await SharedPreferences.getInstance();
const key = '${_lastUpdatedKeyPrefix}discharge';
Comment thread
AliAkrem marked this conversation as resolved.
Outdated

final timestamp = prefs.getString(key);
if (timestamp == null) return null;

return DateTime.parse(timestamp);
} catch (e) {
print('Error getting last updated time: $e');
return null;
}
}

// Clear discharge cache
Future<bool> clearCache() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_dischargeKey);
await prefs.remove('${_lastUpdatedKeyPrefix}discharge');
return true;
} catch (e) {
print('Error clearing discharge cache: $e');
return false;
}
}
}
Loading
Loading