diff --git a/README.md b/README.md index 4e17b30..da6c668 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ So I decided to rebuild it from scratch, with a better design and offline suppor Assessment Screen Exams Screen Timeline Screen + Discharge Screen

## App Modules @@ -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 diff --git a/lib/app.dart b/lib/app.dart index 9b38317..d5c6036 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -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 { @@ -32,6 +33,7 @@ class ProgresApp extends StatelessWidget { BlocProvider(create: (context) => injector()), BlocProvider(create: (context) => injector()), BlocProvider(create: (context) => injector()), + BlocProvider(create: (context) => injector()), ], child: CalendarControllerProvider( controller: EventController(), diff --git a/lib/config/routes/app_router.dart b/lib/config/routes/app_router.dart index 6f3908d..7f3a77e 100644 --- a/lib/config/routes/app_router.dart +++ b/lib/config/routes/app_router.dart @@ -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'; @@ -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 @@ -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; @@ -108,6 +111,11 @@ class AppRouter { name: transcripts, builder: (context, state) => const TranscriptPage(), ), + GoRoute( + path: dischargePath, + name: discharge, + builder: (context, state) => const DischargePage(), + ), ], ), GoRoute( diff --git a/lib/core/di/injector.dart b/lib/core/di/injector.dart index fd302e2..2636e28 100644 --- a/lib/core/di/injector.dart +++ b/lib/core/di/injector.dart @@ -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; @@ -53,6 +55,7 @@ Future initDependencies() async { injector.registerLazySingleton( () => AcademicPerformencetRepositoryImpl(apiClient: injector()), ); + injector.registerLazySingleton(() => StudentDischargeRepositoryImpl()); injector.registerLazySingleton(() => TimelineCacheService()); injector.registerLazySingleton(() => EnrollmentCacheService()); injector.registerLazySingleton(() => TranscriptCacheService()); @@ -105,4 +108,7 @@ Future initDependencies() async { cacheService: injector(), ), ); + injector.registerFactory( + () => StudentDischargeBloc(studentDischargeRepository: injector()), + ); } diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 14ccff7..b22f9e0 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -37,6 +37,10 @@ class ApiClient { }, ), ); + _initializeCacheManager(); + } + + void _initializeCacheManager() { CacheManager.getInstance().then((value) => _cacheManager = value); } diff --git a/lib/features/academics/data/services/academics_cache_service.dart b/lib/features/academics/data/services/academics_cache_service.dart index 5ff769e..b787c45 100644 --- a/lib/features/academics/data/services/academics_cache_service.dart +++ b/lib/features/academics/data/services/academics_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AcademicsCacheService { @@ -20,7 +21,7 @@ class AcademicsCacheService { ); return true; } catch (e) { - print('Error caching academics data: $e'); + debugPrint('Error caching academics data: $e'); return false; } } @@ -35,7 +36,7 @@ class AcademicsCacheService { return jsonDecode(dataString) as List; } catch (e) { - print('Error retrieving cached academics data: $e'); + debugPrint('Error retrieving cached academics data: $e'); return null; } } @@ -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; } } @@ -70,7 +71,7 @@ class AcademicsCacheService { } return true; } catch (e) { - print('Error clearing academics cache: $e'); + debugPrint('Error clearing academics cache: $e'); return false; } } diff --git a/lib/features/auth/presentation/bloc/auth_bloc.dart b/lib/features/auth/presentation/bloc/auth_bloc.dart index 3c2f935..a2d0e72 100644 --- a/lib/features/auth/presentation/bloc/auth_bloc.dart +++ b/lib/features/auth/presentation/bloc/auth_bloc.dart @@ -81,35 +81,35 @@ class AuthBloc extends Bloc { try { event.context?.read().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().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().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().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().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().add(ClearProfileCacheEvent()); } catch (e) { - print('Note: Could not clear profile cache. ${e.toString()}'); + debugPrint('Note: Could not clear profile cache. ${e.toString()}'); } emit(AuthLoggedOut()); diff --git a/lib/features/dashboard/presentation/widgets/dashboard.dart b/lib/features/dashboard/presentation/widgets/dashboard.dart index 97c5e73..9e64a40 100644 --- a/lib/features/dashboard/presentation/widgets/dashboard.dart +++ b/lib/features/dashboard/presentation/widgets/dashboard.dart @@ -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), + ), ], ), diff --git a/lib/features/discharge/data/models/discharge.dart b/lib/features/discharge/data/models/discharge.dart new file mode 100644 index 0000000..7c887ef --- /dev/null +++ b/lib/features/discharge/data/models/discharge.dart @@ -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 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 toJson() { + return { + 'sitDep': sitDep, + 'sitBf': sitBf, + 'sitBc': sitBc, + 'sitRu': sitRu, + 'sitBrs': sitBrs, + }; + } +} diff --git a/lib/features/discharge/data/repository/discharge_repository_impl.dart b/lib/features/discharge/data/repository/discharge_repository_impl.dart new file mode 100644 index 0000000..1ba3051 --- /dev/null +++ b/lib/features/discharge/data/repository/discharge_repository_impl.dart @@ -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 getStudentDischarge() async { + try { + final uuid = await _apiClient.getUuid(); + final response = await _apiClient.get('/$uuid/qitus'); + + final List 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; +} diff --git a/lib/features/discharge/data/services/discharge_api_client.dart b/lib/features/discharge/data/services/discharge_api_client.dart new file mode 100644 index 0000000..c550134 --- /dev/null +++ b/lib/features/discharge/data/services/discharge_api_client.dart @@ -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'; + + 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); + } + + Future get isConnected async { + final result = await _connectivity.checkConnectivity(); + return result != ConnectivityResult.none; + } + + Future saveToken(String token) async { + await _secureStorage.write(key: 'auth_token', value: token); + } + + Future saveUuid(String uuid) async { + await _secureStorage.write(key: 'uuid', value: uuid); + } + + Future saveEtablissementId(String etablissementId) async { + await _secureStorage.write(key: 'etablissement_id', value: etablissementId); + } + + Future getUuid() async { + return await _secureStorage.read(key: 'uuid'); + } + + Future getEtablissementId() async { + return await _secureStorage.read(key: 'etablissement_id'); + } + + Future 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? queryParameters) { + final queryStr = + queryParameters != null + ? Uri(queryParameters: queryParameters).query + : ''; + return '$path?$queryStr'; + } + + Future get( + String path, { + Map? 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 post(String path, {dynamic data}) async { + try { + final response = await _dio.post(path, data: data); + return response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/discharge/presentation/bloc/discharge_bloc.dart b/lib/features/discharge/presentation/bloc/discharge_bloc.dart new file mode 100644 index 0000000..dd4f141 --- /dev/null +++ b/lib/features/discharge/presentation/bloc/discharge_bloc.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:progres/features/discharge/data/models/discharge.dart'; +import 'package:progres/features/discharge/data/repository/discharge_repository_impl.dart'; + +class StudentDischargeEvent extends Equatable { + @override + List get props => []; +} + +class LoadStudentDischarge extends StudentDischargeEvent {} + +class StudentDischargeState extends Equatable { + @override + List get props => []; +} + +class StudentDischargeInitial extends StudentDischargeState {} + +class StudentDischargeLoading extends StudentDischargeState {} + +class StudentDischargeLoaded extends StudentDischargeState { + final StudentDischarge studentDischarge; + + StudentDischargeLoaded({required this.studentDischarge}); + + @override + List get props => [studentDischarge]; +} + +class StudentDischargeNotRequired extends StudentDischargeState {} + +class StudentDischargeError extends StudentDischargeState { + final String message; + + StudentDischargeError(this.message); + + @override + List get props => [message]; +} + +class StudentDischargeBloc + extends Bloc { + final StudentDischargeRepositoryImpl studentDischargeRepository; + + StudentDischargeBloc({required this.studentDischargeRepository}) + : super(StudentDischargeInitial()) { + on(_onLoadStudentDischarge); + } + + Future _onLoadStudentDischarge( + LoadStudentDischarge event, + Emitter emit, + ) async { + emit(StudentDischargeLoading()); + try { + // Always fetch fresh data from API + final studentDischarge = + await studentDischargeRepository.getStudentDischarge(); + + emit(StudentDischargeLoaded(studentDischarge: studentDischarge)); + } on DischargeNotRequiredException { + emit(StudentDischargeNotRequired()); + } catch (e) { + emit(StudentDischargeError(e.toString())); + } + } +} diff --git a/lib/features/discharge/presentation/pages/discharge_page.dart b/lib/features/discharge/presentation/pages/discharge_page.dart new file mode 100644 index 0000000..e5f423d --- /dev/null +++ b/lib/features/discharge/presentation/pages/discharge_page.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart'; +import 'package:progres/features/discharge/presentation/widgets/error.dart'; +import 'package:progres/features/discharge/presentation/widgets/discharge.dart'; +import 'package:flutter_gen/gen_l10n/gallery_localizations.dart'; + +class DischargePage extends StatefulWidget { + const DischargePage({super.key}); + + @override + State createState() => _DischargePageState(); +} + +class _DischargePageState extends State { + @override + void initState() { + super.initState(); + _loadDischarge(); + } + + void _loadDischarge() { + context.read().add(LoadStudentDischarge()); + } + + Future _refreshDischarge() async { + // Reload fresh data from API + context.read().add(LoadStudentDischarge()); + // Simulating network delay for better UX + return Future.delayed(const Duration(milliseconds: 500)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(GalleryLocalizations.of(context)!.myDischarge), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _refreshDischarge, + tooltip: GalleryLocalizations.of(context)!.refreshDischarge, + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + if (state is StudentDischargeLoading) { + return _buildLoadingState(); + } else if (state is StudentDischargeError) { + return ErrorState(state: state); + } else if (state is StudentDischargeLoaded) { + return RefreshIndicator( + onRefresh: _refreshDischarge, + child: DischargeContent(discharge: state.studentDischarge), + ); + } else if (state is StudentDischargeNotRequired) { + return RefreshIndicator( + onRefresh: _refreshDischarge, + child: const DischargeNotRequired(), + ); + } else { + return _buildInitialState(); + } + }, + ), + ); + } + + Widget _buildInitialState() { + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 360; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + GalleryLocalizations.of(context)!.noDischargeData, + style: TextStyle(fontSize: isSmallScreen ? 14 : 16), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadDischarge, + child: Text(GalleryLocalizations.of(context)!.loadDischarge), + ), + ], + ), + ); + } + + Widget _buildLoadingState() { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/features/discharge/presentation/widgets/discharge.dart b/lib/features/discharge/presentation/widgets/discharge.dart new file mode 100644 index 0000000..16c4ccd --- /dev/null +++ b/lib/features/discharge/presentation/widgets/discharge.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:progres/config/theme/app_theme.dart'; +import 'package:progres/features/discharge/data/models/discharge.dart'; +import 'package:flutter_gen/gen_l10n/gallery_localizations.dart'; + +class DischargeContent extends StatelessWidget { + final StudentDischarge discharge; + const DischargeContent({super.key, required this.discharge}); + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 360; + final horizontalPadding = isSmallScreen ? 12.0 : 16.0; + + return SingleChildScrollView( + padding: EdgeInsets.all(horizontalPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderSection(context, isSmallScreen), + SizedBox(height: isSmallScreen ? 16 : 20), + _buildDischargeCards(context, isSmallScreen), + ], + ), + ); + } + + Widget _buildHeaderSection(BuildContext context, bool isSmallScreen) { + final theme = Theme.of(context); + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 12 : 16, + vertical: isSmallScreen ? 12 : 16, + ), + decoration: BoxDecoration( + color: AppTheme.AppPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: isSmallScreen ? 32 : 36, + height: isSmallScreen ? 32 : 36, + decoration: BoxDecoration( + color: AppTheme.AppPrimary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.assignment_turned_in_outlined, + color: AppTheme.AppPrimary, + size: isSmallScreen ? 18 : 20, + ), + ), + SizedBox(width: isSmallScreen ? 10 : 12), + Expanded( + child: Text( + GalleryLocalizations.of(context)!.dischargeStatus, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: AppTheme.AppPrimary, + fontSize: isSmallScreen ? 16 : 18, + ), + ), + ), + ], + ), + SizedBox(height: isSmallScreen ? 6 : 8), + Text( + GalleryLocalizations.of(context)!.dischargeStatusDescription, + style: TextStyle( + fontSize: isSmallScreen ? 12 : 14, + color: + theme.brightness == Brightness.light + ? Colors.grey.shade700 + : Colors.grey.shade300, + ), + ), + ], + ), + ); + } + + Widget _buildDischargeCards(BuildContext context, bool isSmallScreen) { + return Column( + children: [ + DischargeCard( + title: GalleryLocalizations.of(context)!.departmentLevel, + description: GalleryLocalizations.of(context)!.departmentDescription, + isCleared: discharge.sitDep, + icon: Icons.domain_outlined, + isSmallScreen: isSmallScreen, + ), + SizedBox(height: isSmallScreen ? 10 : 12), + DischargeCard( + title: GalleryLocalizations.of(context)!.facultyLibraryLevel, + description: + GalleryLocalizations.of(context)!.facultyLibraryDescription, + isCleared: discharge.sitBf, + icon: Icons.local_library_outlined, + isSmallScreen: isSmallScreen, + ), + SizedBox(height: isSmallScreen ? 10 : 12), + DischargeCard( + title: GalleryLocalizations.of(context)!.centralLibraryLevel, + description: + GalleryLocalizations.of(context)!.centralLibraryDescription, + isCleared: discharge.sitBc, + icon: Icons.library_books_outlined, + isSmallScreen: isSmallScreen, + ), + SizedBox(height: isSmallScreen ? 10 : 12), + DischargeCard( + title: GalleryLocalizations.of(context)!.residenceLevel, + description: GalleryLocalizations.of(context)!.residenceDescription, + isCleared: discharge.sitRu, + icon: Icons.home_outlined, + isSmallScreen: isSmallScreen, + ), + SizedBox(height: isSmallScreen ? 10 : 12), + DischargeCard( + title: GalleryLocalizations.of(context)!.scholarshipServiceLevel, + description: + GalleryLocalizations.of(context)!.scholarshipServiceDescription, + isCleared: discharge.sitBrs, + icon: Icons.school_outlined, + isSmallScreen: isSmallScreen, + ), + ], + ); + } +} + +class DischargeCard extends StatelessWidget { + final String title; + final String description; + final bool isCleared; + final IconData icon; + final bool isSmallScreen; + + const DischargeCard({ + super.key, + required this.title, + required this.description, + required this.isCleared, + required this.icon, + required this.isSmallScreen, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final statusColor = isCleared ? Colors.green : Colors.orange; + final statusIcon = isCleared ? Icons.check_circle : Icons.pending; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: + theme.brightness == Brightness.light + ? AppTheme.AppBorder + : Colors.grey.shade800, + ), + ), + child: Padding( + padding: EdgeInsets.all(isSmallScreen ? 14 : 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: isSmallScreen ? 36 : 40, + height: isSmallScreen ? 36 : 40, + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: statusColor, + size: isSmallScreen ? 20 : 24, + ), + ), + SizedBox(width: isSmallScreen ? 12 : 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + fontSize: isSmallScreen ? 14 : 16, + ), + ), + SizedBox(height: isSmallScreen ? 2 : 4), + Text( + description, + style: TextStyle( + fontSize: isSmallScreen ? 11 : 12, + color: + theme.brightness == Brightness.light + ? Colors.grey.shade600 + : Colors.grey.shade400, + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 8 : 10, + vertical: isSmallScreen ? 4 : 6, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + statusIcon, + size: isSmallScreen ? 14 : 16, + color: statusColor, + ), + SizedBox(width: isSmallScreen ? 4 : 6), + Text( + isCleared + ? GalleryLocalizations.of(context)!.cleared + : GalleryLocalizations.of(context)!.pending, + style: TextStyle( + fontSize: isSmallScreen ? 10 : 12, + fontWeight: FontWeight.w600, + color: statusColor, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class EmptyDischarge extends StatelessWidget { + const EmptyDischarge({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 360; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.assignment_outlined, + size: isSmallScreen ? 56 : 64, + color: theme.disabledColor, + ), + const SizedBox(height: 16), + Text( + GalleryLocalizations.of(context)!.noDischargeData, + style: theme.textTheme.titleLarge?.copyWith( + fontSize: isSmallScreen ? 18 : 20, + ), + ), + const SizedBox(height: 8), + Text( + GalleryLocalizations.of(context)!.dischargeNotAvailable, + textAlign: TextAlign.center, + style: TextStyle( + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.7), + fontSize: isSmallScreen ? 14 : 16, + ), + ), + ], + ), + ); + } +} + +class DischargeNotRequired extends StatelessWidget { + const DischargeNotRequired({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 360; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: isSmallScreen ? 80 : 96, + height: isSmallScreen ? 80 : 96, + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_circle_outline, + size: isSmallScreen ? 48 : 56, + color: Colors.green, + ), + ), + const SizedBox(height: 24), + Text( + GalleryLocalizations.of(context)!.dischargeNotRequiredTitle, + style: theme.textTheme.titleLarge?.copyWith( + fontSize: isSmallScreen ? 18 : 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Padding( + padding: EdgeInsets.symmetric(horizontal: isSmallScreen ? 24 : 32), + child: Text( + GalleryLocalizations.of(context)!.dischargeNotRequiredDescription, + textAlign: TextAlign.center, + style: TextStyle( + color: theme.textTheme.bodyMedium?.color?.withValues( + alpha: 0.7, + ), + fontSize: isSmallScreen ? 14 : 16, + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/discharge/presentation/widgets/error.dart b/lib/features/discharge/presentation/widgets/error.dart new file mode 100644 index 0000000..5b762b9 --- /dev/null +++ b/lib/features/discharge/presentation/widgets/error.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:progres/features/discharge/presentation/bloc/discharge_bloc.dart'; +import 'package:flutter_gen/gen_l10n/gallery_localizations.dart'; + +class ErrorState extends StatelessWidget { + final StudentDischargeError state; + + const ErrorState({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + final isSmallScreen = screenSize.width < 360; + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: isSmallScreen ? 16.0 : 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + GalleryLocalizations.of(context)!.somthingWentWrong, + textAlign: TextAlign.center, + style: TextStyle(fontSize: isSmallScreen ? 14 : 16), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + LoadStudentDischarge(), + ); + }, + child: Text(GalleryLocalizations.of(context)!.retry), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/enrollment/data/services/enrollment_cache_service.dart b/lib/features/enrollment/data/services/enrollment_cache_service.dart index 6069f19..98ba9f4 100644 --- a/lib/features/enrollment/data/services/enrollment_cache_service.dart +++ b/lib/features/enrollment/data/services/enrollment_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:progres/features/enrollment/data/models/enrollment.dart'; @@ -19,7 +20,7 @@ class EnrollmentCacheService { ); return true; } catch (e) { - print('Error caching enrollments: $e'); + debugPrint('Error caching enrollments: $e'); return false; } } @@ -35,7 +36,7 @@ class EnrollmentCacheService { final List decodedJson = jsonDecode(enrollmentsString); return decodedJson.map((json) => Enrollment.fromJson(json)).toList(); } catch (e) { - print('Error retrieving cached enrollments: $e'); + debugPrint('Error retrieving cached enrollments: $e'); return null; } } @@ -51,7 +52,7 @@ class EnrollmentCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -64,7 +65,7 @@ class EnrollmentCacheService { await prefs.remove('${_lastUpdatedKeyPrefix}enrollments'); return true; } catch (e) { - print('Error clearing enrollment cache: $e'); + debugPrint('Error clearing enrollment cache: $e'); return false; } } diff --git a/lib/features/enrollment/presentation/bloc/enrollment_bloc.dart b/lib/features/enrollment/presentation/bloc/enrollment_bloc.dart index ec7deb2..c58f14a 100644 --- a/lib/features/enrollment/presentation/bloc/enrollment_bloc.dart +++ b/lib/features/enrollment/presentation/bloc/enrollment_bloc.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:progres/features/enrollment/data/repositories/enrollment_repository_impl.dart'; import 'package:progres/features/enrollment/data/services/enrollment_cache_service.dart'; @@ -41,7 +42,7 @@ class EnrollmentBloc extends Bloc { emit(EnrollmentsLoaded(enrollments: enrollments, fromCache: false)); } catch (e) { - print('Error loading enrollments: $e'); + debugPrint('Error loading enrollments: $e'); final cachedEnrollments = await cacheService.getCachedEnrollments(); if (cachedEnrollments != null && cachedEnrollments.isNotEmpty) { diff --git a/lib/features/groups/data/services/groups_cache_service.dart b/lib/features/groups/data/services/groups_cache_service.dart index a3327eb..ce2836e 100644 --- a/lib/features/groups/data/services/groups_cache_service.dart +++ b/lib/features/groups/data/services/groups_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:progres/features/groups/data/models/group.dart'; @@ -19,7 +20,7 @@ class GroupsCacheService { ); return true; } catch (e) { - print('Error caching groups: $e'); + debugPrint('Error caching groups: $e'); return false; } } @@ -35,7 +36,7 @@ class GroupsCacheService { final List decodedJson = jsonDecode(groupsString); return decodedJson.map((json) => StudentGroup.fromJson(json)).toList(); } catch (e) { - print('Error retrieving cached groups: $e'); + debugPrint('Error retrieving cached groups: $e'); return null; } } @@ -51,7 +52,7 @@ class GroupsCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -64,7 +65,7 @@ class GroupsCacheService { await prefs.remove('${_lastUpdatedKeyPrefix}groups'); return true; } catch (e) { - print('Error clearing groups cache: $e'); + debugPrint('Error clearing groups cache: $e'); return false; } } diff --git a/lib/features/profile/data/services/profile_cache_service.dart b/lib/features/profile/data/services/profile_cache_service.dart index a8ab523..c471843 100644 --- a/lib/features/profile/data/services/profile_cache_service.dart +++ b/lib/features/profile/data/services/profile_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ProfileCacheService { @@ -14,7 +15,7 @@ class ProfileCacheService { await prefs.setString(_lastUpdatedKey, DateTime.now().toIso8601String()); return true; } catch (e) { - print('Error caching profile data: $e'); + debugPrint('Error caching profile data: $e'); return false; } } @@ -29,7 +30,7 @@ class ProfileCacheService { return jsonDecode(profileDataString) as Map; } catch (e) { - print('Error retrieving cached profile data: $e'); + debugPrint('Error retrieving cached profile data: $e'); return null; } } @@ -43,7 +44,7 @@ class ProfileCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -56,7 +57,7 @@ class ProfileCacheService { await prefs.remove(_lastUpdatedKey); return true; } catch (e) { - print('Error clearing profile cache: $e'); + debugPrint('Error clearing profile cache: $e'); return false; } } diff --git a/lib/features/subject/data/services/subject_cache_service.dart b/lib/features/subject/data/services/subject_cache_service.dart index 670e44d..3917a5e 100644 --- a/lib/features/subject/data/services/subject_cache_service.dart +++ b/lib/features/subject/data/services/subject_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:progres/features/subject/data/models/course_coefficient.dart'; @@ -35,7 +36,7 @@ class SubjectCacheService { await prefs.setString(lastUpdatedKey, DateTime.now().toIso8601String()); return true; } catch (e) { - print('Error caching subject coefficients: $e'); + debugPrint('Error caching subject coefficients: $e'); return false; } } @@ -57,7 +58,7 @@ class SubjectCacheService { .map((json) => CourseCoefficient.fromJson(json)) .toList(); } catch (e) { - print('Error retrieving cached subject coefficients: $e'); + debugPrint('Error retrieving cached subject coefficients: $e'); return null; } } @@ -76,7 +77,7 @@ class SubjectCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -98,7 +99,7 @@ class SubjectCacheService { await prefs.remove(lastUpdatedKey); return true; } catch (e) { - print('Error clearing specific subject cache: $e'); + debugPrint('Error clearing specific subject cache: $e'); return false; } } @@ -117,7 +118,7 @@ class SubjectCacheService { } return true; } catch (e) { - print('Error clearing all subject caches: $e'); + debugPrint('Error clearing all subject caches: $e'); return false; } } diff --git a/lib/features/subject/presentation/bloc/subject_bloc.dart b/lib/features/subject/presentation/bloc/subject_bloc.dart index b605161..1b5f03f 100644 --- a/lib/features/subject/presentation/bloc/subject_bloc.dart +++ b/lib/features/subject/presentation/bloc/subject_bloc.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:progres/features/subject/data/repositories/subject_repository_impl.dart'; @@ -121,7 +122,7 @@ class SubjectBloc extends Bloc { await cacheService.clearAllCache(); } } catch (e) { - print('Error clearing subject cache: $e'); + debugPrint('Error clearing subject cache: $e'); } } } diff --git a/lib/features/timeline/data/models/course_session.dart b/lib/features/timeline/data/models/course_session.dart index 8c09d7b..f88e642 100644 --- a/lib/features/timeline/data/models/course_session.dart +++ b/lib/features/timeline/data/models/course_session.dart @@ -113,7 +113,7 @@ class CourseSession { // Print debug info for Sunday specifically if (jourId == 2) { // Sunday - print( + debugPrint( 'SUNDAY EVENT: jourId $jourId ($jourLibelleFr) mapped to ${_formatDate(result)} (weekday: ${result.weekday})', ); } diff --git a/lib/features/timeline/data/services/timeline_cache_service.dart b/lib/features/timeline/data/services/timeline_cache_service.dart index c12af67..a4b9325 100644 --- a/lib/features/timeline/data/services/timeline_cache_service.dart +++ b/lib/features/timeline/data/services/timeline_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; class TimelineCacheService { @@ -23,7 +24,7 @@ class TimelineCacheService { ); return true; } catch (e) { - print('Error caching timeline events: $e'); + debugPrint('Error caching timeline events: $e'); return false; } } @@ -42,7 +43,7 @@ class TimelineCacheService { return jsonDecode(eventsString) as List; } catch (e) { - print('Error retrieving cached timeline events: $e'); + debugPrint('Error retrieving cached timeline events: $e'); return null; } } @@ -58,7 +59,7 @@ class TimelineCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -77,7 +78,7 @@ class TimelineCacheService { } return true; } catch (e) { - print('Error clearing timeline cache: $e'); + debugPrint('Error clearing timeline cache: $e'); return false; } } diff --git a/lib/features/transcript/data/services/transcript_cache_service.dart b/lib/features/transcript/data/services/transcript_cache_service.dart index eb72ec6..c89bc42 100644 --- a/lib/features/transcript/data/services/transcript_cache_service.dart +++ b/lib/features/transcript/data/services/transcript_cache_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:progres/features/transcript/data/models/academic_transcript.dart'; import 'package:progres/features/transcript/data/models/annual_transcript_summary.dart'; @@ -27,7 +28,7 @@ class TranscriptCacheService { ); return true; } catch (e) { - print('Error caching transcripts: $e'); + debugPrint('Error caching transcripts: $e'); return false; } } @@ -49,7 +50,7 @@ class TranscriptCacheService { .map((json) => AcademicTranscript.fromJson(json)) .toList(); } catch (e) { - print('Error retrieving cached transcripts: $e'); + debugPrint('Error retrieving cached transcripts: $e'); return null; } } @@ -71,7 +72,7 @@ class TranscriptCacheService { ); return true; } catch (e) { - print('Error caching annual summary: $e'); + debugPrint('Error caching annual summary: $e'); return false; } } @@ -91,7 +92,7 @@ class TranscriptCacheService { final decodedJson = jsonDecode(summaryString); return AnnualTranscriptSummary.fromJson(decodedJson); } catch (e) { - print('Error retrieving cached annual summary: $e'); + debugPrint('Error retrieving cached annual summary: $e'); return null; } } @@ -107,7 +108,7 @@ class TranscriptCacheService { return DateTime.parse(timestamp); } catch (e) { - print('Error getting last updated time: $e'); + debugPrint('Error getting last updated time: $e'); return null; } } @@ -128,7 +129,7 @@ class TranscriptCacheService { } return true; } catch (e) { - print('Error clearing cache: $e'); + debugPrint('Error clearing cache: $e'); return false; } } diff --git a/lib/features/transcript/presentation/bloc/transcript_bloc.dart b/lib/features/transcript/presentation/bloc/transcript_bloc.dart index deefa2e..caa0c3d 100644 --- a/lib/features/transcript/presentation/bloc/transcript_bloc.dart +++ b/lib/features/transcript/presentation/bloc/transcript_bloc.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:progres/features/transcript/data/models/annual_transcript_summary.dart'; import 'package:progres/features/transcript/data/repositories/transcript_repository_impl.dart'; @@ -54,7 +55,7 @@ class TranscriptBloc extends Bloc { emit(EnrollmentsLoaded(enrollments: enrollments, fromCache: false)); } catch (e) { - print('Error loading enrollments: $e'); + debugPrint('Error loading enrollments: $e'); final cachedEnrollments = await enrollmentCacheService.getCachedEnrollments(); @@ -91,7 +92,7 @@ class TranscriptBloc extends Bloc { .getCachedAnnualSummary(event.enrollmentId); if (cachedTranscripts != null && cachedTranscripts.isNotEmpty) { - print( + debugPrint( 'Using cached transcripts and summary for enrollment ID: ${event.enrollmentId}', ); emit( @@ -132,7 +133,7 @@ class TranscriptBloc extends Bloc { annualSummary, ); } catch (e) { - print('Error loading annual summary: $e'); + debugPrint('Error loading annual summary: $e'); // Try to get from cache if network request fails annualSummary = await transcriptCacheService.getCachedAnnualSummary( event.enrollmentId, @@ -148,7 +149,7 @@ class TranscriptBloc extends Bloc { ), ); } catch (e) { - print('Error loading transcripts: $e'); + debugPrint('Error loading transcripts: $e'); // Try to load from cache if network request fails final cachedTranscripts = await transcriptCacheService @@ -222,7 +223,7 @@ class TranscriptBloc extends Bloc { ); } } catch (e) { - print('Error loading annual summary: $e'); + debugPrint('Error loading annual summary: $e'); // Don't change state on error, just log } } diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 3dcfd28..3470cee 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -552,5 +552,89 @@ "viewonGitHub": "عرض على github", "@viewonGitHub": { "description": "رابط إلى github" + }, + "myDischarge": "براءة الذمة", + "@myDischarge": { + "description": "عنوان صفحة براءة الذمة" + }, + "refreshDischarge": "تحديث براءة الذمة", + "@refreshDischarge": { + "description": "تلميح لزر تحديث براءة الذمة" + }, + "loadDischarge": "تحميل براءة الذمة", + "@loadDischarge": { + "description": "نص زر تحميل براءة الذمة" + }, + "noDischargeData": "لا توجد بيانات براءة ذمة متاحة", + "@noDischargeData": { + "description": "رسالة تظهر عندما لا تتوفر بيانات براءة الذمة" + }, + "dischargeNotAvailable": "معلومات براءة الذمة غير متاحة في الوقت الحالي.", + "@dischargeNotAvailable": { + "description": "وصف يظهر عندما لا تتوفر بيانات براءة الذمة" + }, + "dischargeStatus": "حالة براءة الذمة", + "@dischargeStatus": { + "description": "عنوان قسم حالة براءة الذمة" + }, + "dischargeStatusDescription": "حالة براءة الذمة الحالية عبر جميع خدمات الجامعة", + "@dischargeStatusDescription": { + "description": "وصف قسم حالة براءة الذمة" + }, + "departmentLevel": "مستوى القسم", + "@departmentLevel": { + "description": "عنوان براءة الذمة على مستوى القسم" + }, + "departmentDescription": "تسوية القسم الأكاديمي", + "@departmentDescription": { + "description": "وصف براءة الذمة على مستوى القسم" + }, + "facultyLibraryLevel": "مستوى مكتبة الكلية", + "@facultyLibraryLevel": { + "description": "عنوان براءة الذمة على مستوى مكتبة الكلية" + }, + "facultyLibraryDescription": "تسوية كتب ومواد مكتبة الكلية", + "@facultyLibraryDescription": { + "description": "وصف براءة الذمة على مستوى مكتبة الكلية" + }, + "centralLibraryLevel": "مستوى المكتبة المركزية", + "@centralLibraryLevel": { + "description": "عنوان براءة الذمة على مستوى المكتبة المركزية" + }, + "centralLibraryDescription": "تسوية كتب ومواد المكتبة المركزية", + "@centralLibraryDescription": { + "description": "وصف براءة الذمة على مستوى المكتبة المركزية" + }, + "residenceLevel": "مستوى الإقامة", + "@residenceLevel": { + "description": "عنوان براءة الذمة على مستوى الإقامة" + }, + "residenceDescription": "تسوية سكن الإقامة الجامعية", + "@residenceDescription": { + "description": "وصف براءة الذمة على مستوى الإقامة" + }, + "scholarshipServiceLevel": "مستوى خدمة المنح الدراسية", + "@scholarshipServiceLevel": { + "description": "عنوان براءة الذمة على مستوى خدمة المنح الدراسية" + }, + "scholarshipServiceDescription": "تسوية المنح الدراسية والمساعدات المالية", + "@scholarshipServiceDescription": { + "description": "وصف براءة الذمة على مستوى خدمة المنح الدراسية" + }, + "cleared": "مسوى", + "@cleared": { + "description": "حالة عندما تكون براءة الذمة مسواة" + }, + "pending": "في الانتظار", + "@pending": { + "description": "حالة عندما تكون براءة الذمة في الانتظار" + }, + "dischargeNotRequiredTitle": "لا تحتاج براءة ذمة", + "@dischargeNotRequiredTitle": { + "description": "عنوان عندما لا تكون براءة الذمة مطلوبة للطالب" + }, + "dischargeNotRequiredDescription": "لست مطالباً بإكمال أي إجراءات براءة ذمة في الوقت الحالي. وضعك الأكاديمي لا يتطلب تسوية من خدمات الجامعة.", + "@dischargeNotRequiredDescription": { + "description": "وصف عندما لا تكون براءة الذمة مطلوبة للطالب" } -} \ No newline at end of file +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 50131dc..99cd67a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -533,5 +533,89 @@ "viewonGitHub": "View on GitHub", "@viewonGitHub": { "description": "link to github" + }, + "myDischarge": "My Discharge", + "@myDischarge": { + "description": "Title for discharge page" + }, + "refreshDischarge": "Refresh discharge", + "@refreshDischarge": { + "description": "Tooltip for refresh discharge button" + }, + "loadDischarge": "Load Discharge", + "@loadDischarge": { + "description": "Button text to load discharge" + }, + "noDischargeData": "No discharge data available", + "@noDischargeData": { + "description": "Message shown when no discharge data is available" + }, + "dischargeNotAvailable": "Discharge information is not available at this time.", + "@dischargeNotAvailable": { + "description": "Description shown when discharge data is not available" + }, + "dischargeStatus": "Discharge Status", + "@dischargeStatus": { + "description": "Title for discharge status section" + }, + "dischargeStatusDescription": "Your current discharge status across all university services", + "@dischargeStatusDescription": { + "description": "Description for discharge status section" + }, + "departmentLevel": "Department Level", + "@departmentLevel": { + "description": "Title for department level discharge" + }, + "departmentDescription": "Academic department clearance", + "@departmentDescription": { + "description": "Description for department level discharge" + }, + "facultyLibraryLevel": "Faculty Library Level", + "@facultyLibraryLevel": { + "description": "Title for faculty library level discharge" + }, + "facultyLibraryDescription": "Faculty library books and materials clearance", + "@facultyLibraryDescription": { + "description": "Description for faculty library level discharge" + }, + "centralLibraryLevel": "Central Library Level", + "@centralLibraryLevel": { + "description": "Title for central library level discharge" + }, + "centralLibraryDescription": "Central library books and materials clearance", + "@centralLibraryDescription": { + "description": "Description for central library level discharge" + }, + "residenceLevel": "Residence Level", + "@residenceLevel": { + "description": "Title for residence level discharge" + }, + "residenceDescription": "University residence accommodation clearance", + "@residenceDescription": { + "description": "Description for residence level discharge" + }, + "scholarshipServiceLevel": "Scholarship Service Level", + "@scholarshipServiceLevel": { + "description": "Title for scholarship service level discharge" + }, + "scholarshipServiceDescription": "Scholarship and financial aid clearance", + "@scholarshipServiceDescription": { + "description": "Description for scholarship service level discharge" + }, + "cleared": "Cleared", + "@cleared": { + "description": "Status when discharge is cleared" + }, + "pending": "Pending", + "@pending": { + "description": "Status when discharge is pending" + }, + "dischargeNotRequiredTitle": "No Discharge Required", + "@dischargeNotRequiredTitle": { + "description": "Title when discharge is not required for the student" + }, + "dischargeNotRequiredDescription": "You are not required to complete any discharge procedures at this time. Your academic status does not require clearance from university services.", + "@dischargeNotRequiredDescription": { + "description": "Description when discharge is not required for the student" } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 1aa2d25..778ebdc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.1.0+2 environment: sdk: ^3.7.2 - flutter: "3.22.3" # Updated to latest stable version + flutter: "3.22.3" dependencies: flutter: diff --git a/screenshot/discharge.jpg b/screenshot/discharge.jpg new file mode 100644 index 0000000..4aabb4e Binary files /dev/null and b/screenshot/discharge.jpg differ