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
2 changes: 2 additions & 0 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:progres/core/di/injector.dart';
import 'package:progres/core/theme/theme_bloc.dart';
import 'package:progres/features/academics/presentation/bloc/academics_bloc.dart';
import 'package:progres/features/auth/presentation/bloc/auth_bloc.dart';
import 'package:progres/features/debts/presentation/bloc/debts_bloc.dart';
import 'package:progres/features/enrollment/presentation/bloc/enrollment_bloc.dart';
import 'package:progres/features/groups/presentation/bloc/groups_bloc.dart';
import 'package:progres/features/profile/presentation/bloc/profile_bloc.dart';
Expand All @@ -34,6 +35,7 @@ class ProgresApp extends StatelessWidget {
BlocProvider(create: (context) => injector<TranscriptBloc>()),
BlocProvider(create: (context) => injector<EnrollmentBloc>()),
BlocProvider(create: (context) => injector<StudentDischargeBloc>()),
BlocProvider(create: (context) => injector<DebtsBloc>()),
],
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/core/splash_screen/splash_screen.dart';
import 'package:progres/features/academics/presentation/pages/academic_performance_page.dart';
import 'package:progres/features/debts/presentation/pages/debts_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';
Expand Down Expand Up @@ -32,6 +33,7 @@ class AppRouter {
static const String transcripts = 'transcripts';
static const String discharge = 'discharge';
static const String about = 'about';
static const String debts = 'debts';

// Route paths
static const String splashPath = '/';
Expand All @@ -48,6 +50,7 @@ class AppRouter {
static const String transcriptsPath = 'transcripts';
static const String dischargePath = 'discharge';
static const String aboutPath = 'about';
static const String debtsPath = 'debts';

late final GoRouter router;

Expand Down Expand Up @@ -129,6 +132,11 @@ class AppRouter {
name: discharge,
builder: (context, state) => const DischargePage(),
),
GoRoute(
path: debtsPath,
name: debts,
builder: (context, state) => const DebtsPage(),
),
],
),
GoRoute(
Expand Down
12 changes: 11 additions & 1 deletion lib/core/di/injector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ import 'package:progres/features/transcript/data/services/transcript_cache_servi
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';
import 'package:progres/features/debts/data/repositories/debts_repository_impl.dart';
import 'package:progres/features/debts/data/services/debts_cache_service.dart';
import 'package:progres/features/debts/presentation/bloc/debts_bloc.dart';

final injector = GetIt.instance;

Future<void> initDependencies() async {
injector.registerLazySingleton(() => ApiClient());
injector.registerLazySingleton(() => ApiClient());
injector.registerLazySingleton(
() => AuthRepositoryImpl(apiClient: injector()),
);
Expand All @@ -55,11 +58,15 @@ Future<void> initDependencies() async {
() => AcademicPerformanceRepositoryImpl(apiClient: injector()),
);
injector.registerLazySingleton(() => StudentDischargeRepositoryImpl());
injector.registerLazySingleton(
() => DebtsRepositoryImpl(apiClient: injector()),
);
injector.registerLazySingleton(() => TimelineCacheService());
injector.registerLazySingleton(() => EnrollmentCacheService());
injector.registerLazySingleton(() => TranscriptCacheService());
injector.registerLazySingleton(() => GroupsCacheService());
injector.registerLazySingleton(() => SubjectCacheService());
injector.registerLazySingleton(() => DebtsCacheService());

// Register BLoCs
injector.registerFactory(() => ThemeBloc()..add(LoadTheme()));
Expand Down Expand Up @@ -110,4 +117,7 @@ Future<void> initDependencies() async {
injector.registerFactory(
() => StudentDischargeBloc(studentDischargeRepository: injector()),
);
injector.registerFactory(
() => DebtsBloc(debtsRepository: injector(), debtsCacheService: injector()),
);
}
7 changes: 7 additions & 0 deletions lib/features/auth/presentation/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:progres/features/auth/data/repositories/auth_repository_impl.dart';
import 'package:progres/features/auth/data/models/auth_response.dart';
import 'package:progres/features/debts/presentation/bloc/debts_bloc.dart';
import 'package:progres/features/debts/presentation/bloc/debts_event.dart';
import 'package:progres/features/enrollment/presentation/bloc/enrollment_bloc.dart';
import 'package:progres/features/enrollment/presentation/bloc/enrollment_event.dart';
import 'package:progres/features/groups/presentation/bloc/groups_bloc.dart';
Expand Down Expand Up @@ -111,6 +113,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} catch (e) {
debugPrint('Note: Could not clear profile cache. ${e.toString()}');
}
try {
event.context?.read<DebtsBloc>().add(ClearDebtsCache());
} catch (e) {
debugPrint('Note: Could not clear debts cache. ${e.toString()}');
}

emit(AuthLoggedOut());
} catch (e) {
Expand Down
8 changes: 8 additions & 0 deletions lib/features/dashboard/presentation/widgets/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ Widget buildDashboard(ProfileLoaded state, BuildContext context) {
color: AppTheme.AppPrimary,
onTap: () => context.goNamed(AppRouter.discharge),
),

buildGridCard(
context,
title: AppLocalizations.of(context)!.academicDebts,
icon: Icons.assignment_late,
color: AppTheme.AppPrimary,
onTap: () => context.goNamed(AppRouter.debts),
),
],
),

Expand Down
35 changes: 35 additions & 0 deletions lib/features/debts/data/models/academic_year_debt.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:progres/features/debts/data/models/debt_course.dart';

class AcademicYearDebt {
final String annee;
final int idAnneeAcademique;
final int rang;
final List<DebtCourse> dette;

AcademicYearDebt({
required this.annee,
required this.idAnneeAcademique,
required this.rang,
required this.dette,
});

factory AcademicYearDebt.fromJson(Map<String, dynamic> json) {
return AcademicYearDebt(
annee: json['annee'] as String,
idAnneeAcademique: json['id_annee_academique'] as int,
rang: json['rang'] as int,
dette: (json['dette'] as List<dynamic>)
.map((e) => DebtCourse.fromJson(e as Map<String, dynamic>))
.toList(),
);
}

Map<String, dynamic> toJson() {
return {
'annee': annee,
'id_annee_academique': idAnneeAcademique,
'rang': rang,
'dette': dette.map((e) => e.toJson()).toList(),
};
}
}
63 changes: 63 additions & 0 deletions lib/features/debts/data/models/debt_course.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
class DebtCourse {
final String mcFr;
final String mcAr;
final String nfr;
final String nar;
final String pfr;
final String par;
final double m;
final double md;
final double cc;
final double ccd;
final double ex;
final double exd;

DebtCourse({
required this.mcFr,
required this.mcAr,
required this.nfr,
required this.nar,
required this.pfr,
required this.par,
required this.m,
required this.md,
required this.cc,
required this.ccd,
required this.ex,
required this.exd,
});

factory DebtCourse.fromJson(Map<String, dynamic> json) {
return DebtCourse(
mcFr: json['mcFr'] as String,
mcAr: json['mcAr'] as String,
nfr: json['nfr'] as String,
nar: json['nar'] as String,
pfr: json['pfr'] as String,
par: json['par'] as String,
m: (json['m'] as num).toDouble(),
md: (json['md'] as num).toDouble(),
cc: (json['cc'] as num).toDouble(),
ccd: (json['ccd'] as num).toDouble(),
ex: (json['ex'] as num).toDouble(),
exd: (json['exd'] as num).toDouble(),
);
}

Map<String, dynamic> toJson() {
return {
'mcFr': mcFr,
'mcAr': mcAr,
'nfr': nfr,
'nar': nar,
'pfr': pfr,
'par': par,
'm': m,
'md': md,
'cc': cc,
'ccd': ccd,
'ex': ex,
'exd': exd,
};
}
}
30 changes: 30 additions & 0 deletions lib/features/debts/data/repositories/debts_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:progres/core/network/api_client.dart';
import 'package:progres/features/debts/data/models/academic_year_debt.dart';

class DebtsRepositoryImpl {
final ApiClient _apiClient;

DebtsRepositoryImpl({ApiClient? apiClient})
: _apiClient = apiClient ?? ApiClient();
Comment on lines +7 to +8
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 | 🟠 Major

Remove the fallback ApiClient() instantiation.

Creating a new ApiClient() when none is provided defeats dependency injection and makes the class difficult to test. According to the DI configuration, apiClient is always injected when registering this repository. Make the parameter required to enforce proper DI usage.

🔎 Proposed fix
-  DebtsRepositoryImpl({ApiClient? apiClient})
-    : _apiClient = apiClient ?? ApiClient();
+  DebtsRepositoryImpl({required ApiClient apiClient})
+    : _apiClient = apiClient;
📝 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
DebtsRepositoryImpl({ApiClient? apiClient})
: _apiClient = apiClient ?? ApiClient();
DebtsRepositoryImpl({required ApiClient apiClient})
: _apiClient = apiClient;
🤖 Prompt for AI Agents
In lib/features/debts/data/repositories/debts_repository_impl.dart around lines
7-8, the constructor currently falls back to creating a new ApiClient() when
none is passed, which breaks DI and testing; change the constructor to require
an ApiClient parameter (remove the default null and the fallback instantiation)
and assign that required ApiClient to _apiClient so the repository always
receives the injected dependency.


Future<List<AcademicYearDebt>> getStudentDebts() async {
try {
final uuid = await _apiClient.getUuid();
if (uuid == null) {
throw Exception('UUID not found, please login again');
}
final response = await _apiClient.get('/infos/dettes/$uuid');
if (response.data == null) {
return [];
}
final List<dynamic> debtsJson = response.data;
final debts = debtsJson
.map((debtJson) => AcademicYearDebt.fromJson(debtJson))
.toList();
debts.sort((a, b) => b.idAnneeAcademique.compareTo(a.idAnneeAcademique));
return debts;
} catch (e) {
rethrow;
}
}
}
80 changes: 80 additions & 0 deletions lib/features/debts/data/services/debts_cache_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:progres/features/debts/data/models/academic_year_debt.dart';

class DebtsCacheService {
// Keys for SharedPreferences
static const String _debtsKey = 'cached_debts';
static const String _lastUpdatedKey = 'last_updated_debts';

// Save debts
Future<bool> cacheDebts(List<AcademicYearDebt> debts) async {
try {
final prefs = await SharedPreferences.getInstance();
final debtsJson = debts.map((d) => d.toJson()).toList();
await prefs.setString(_debtsKey, jsonEncode(debtsJson));
await prefs.setString(_lastUpdatedKey, DateTime.now().toIso8601String());
return true;
} catch (e) {
debugPrint('Error caching debts: $e');
return false;
}
}

// Retrieve debts
Future<List<AcademicYearDebt>?> getCachedDebts() async {
try {
final prefs = await SharedPreferences.getInstance();
final debtsString = prefs.getString(_debtsKey);

if (debtsString == null) return null;

final List<dynamic> decodedJson = jsonDecode(debtsString);
return decodedJson
.map((json) => AcademicYearDebt.fromJson(json))
.toList();
} catch (e) {
debugPrint('Error retrieving cached debts: $e');
return null;
}
}

// Get last update timestamp
Future<DateTime?> getLastUpdated() async {
try {
final prefs = await SharedPreferences.getInstance();
final timestamp = prefs.getString(_lastUpdatedKey);
if (timestamp == null) return null;

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

// Clear all debts cached data
Future<bool> clearCache() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_debtsKey);
await prefs.remove(_lastUpdatedKey);
return true;
} catch (e) {
debugPrint('Error clearing cache: $e');
return false;
}
}

// Check if data is stale (older than specified duration)
Future<bool> isDataStale({
Duration staleDuration = const Duration(hours: 12),
}) async {
final lastUpdated = await getLastUpdated();
if (lastUpdated == null) return true;

final now = DateTime.now();
return now.difference(lastUpdated) > staleDuration;
}
}
Loading