From e1e9ad37da4a4588d8cc123a7fcbc0a71c79d3c8 Mon Sep 17 00:00:00 2001 From: Naresh Pratista <2141720057@student.polinema.ac.id> Date: Wed, 23 Oct 2024 11:16:07 +0700 Subject: [PATCH] refactor(home): improve completed topics API implementation --- lib/core/services/dio_client.dart | 101 ++++- lib/core/services/local_storage_service.dart | 0 .../completed_topics_repository.dart | 26 ++ lib/core/services/repositories/constants.dart | 3 +- .../repositories/exercise_repository.dart | 36 ++ .../repositories/level_repository.dart | 20 +- .../history/models/history_model.dart | 22 +- .../history/provider/history_provider.dart | 50 ++- .../history/screens/history_screen.dart | 203 +++++---- .../history/widgets/custom_tab_bar.dart | 28 +- .../widgets/exercise_history_card.dart | 121 +++-- .../home/models/completed_topics_model.dart | 28 ++ .../provider/completed_topics_provider.dart | 34 ++ lib/features/home/screens/home_screen.dart | 420 ++++++++++++------ .../home/widgets/no_progress_card.dart | 16 + lib/features/home/widgets/progress_bar.dart | 46 +- lib/features/home/widgets/progress_card.dart | 162 +++---- .../exercises/models/exercise_model.dart | 19 + .../providers/exercise_provider.dart | 90 +++- .../exercises/screens/exercise_screen.dart | 10 +- .../exercises/widgets/exercise_navigator.dart | 7 +- .../question/multiple_choice_question.dart | 12 +- .../widgets/question/true_false_question.dart | 11 +- .../feedback/models/feedback_model.dart | 40 ++ .../feedback/providers/feedback_provider.dart | 0 .../feedback/screens/feedback_screen.dart | 85 +++- .../feedback/widgets/feedback_dialog.dart | 5 + .../modules/level/models/level_model.dart | 16 +- .../level/providers/level_provider.dart | 106 ++++- .../level/screens/level_list_screen.dart | 34 +- .../modules/level/widgets/level_card.dart | 24 +- .../modules/level/widgets/pretest_card.dart | 210 +++++---- .../material/screens/material_screen.dart | 36 +- .../material/widgets/video_player_widget.dart | 49 +- .../modules/result/screens/result_screen.dart | 9 +- .../widgets/complete_result_widget.dart | 17 +- .../result/widgets/down_result_widget.dart | 18 +- .../result/widgets/jump_result_widget.dart | 17 +- .../modules/topics/models/topic_model.dart | 12 +- .../topics/screens/topics_list_screen.dart | 19 +- .../modules/topics/widgets/topic_card.dart | 3 +- .../learning/provider/section_provider.dart | 10 +- .../learning/widgets/section_card.dart | 2 +- lib/my_app.dart | 15 +- pubspec.lock | 18 +- pubspec.yaml | 3 - 46 files changed, 1510 insertions(+), 703 deletions(-) create mode 100644 lib/core/services/local_storage_service.dart create mode 100644 lib/core/services/repositories/completed_topics_repository.dart create mode 100644 lib/features/home/models/completed_topics_model.dart create mode 100644 lib/features/home/provider/completed_topics_provider.dart create mode 100644 lib/features/home/widgets/no_progress_card.dart create mode 100644 lib/features/learning/modules/feedback/models/feedback_model.dart create mode 100644 lib/features/learning/modules/feedback/providers/feedback_provider.dart diff --git a/lib/core/services/dio_client.dart b/lib/core/services/dio_client.dart index 96253d3..2c7bde1 100644 --- a/lib/core/services/dio_client.dart +++ b/lib/core/services/dio_client.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'package:dio/dio.dart'; import 'package:english_learning/core/services/repositories/constants.dart'; @@ -10,24 +12,6 @@ class DioClient { _dio.options.receiveTimeout = const Duration(seconds: 3); } - Future post(String path, {dynamic data, Options? options}) async { - try { - final response = await _dio.post( - path, - data: data, - options: - options ?? Options(headers: {'Content-Type': 'application/json'}), - ); - return response; - } on DioError catch (e) { - print('DioError: ${e.response?.data ?? e.message}'); - rethrow; // or handle specific DioError here - } catch (e) { - print('Unexpected error: $e'); - rethrow; - } - } - Future refreshAccessToken(String refreshToken) async { try { final response = await _dio.post( @@ -46,6 +30,24 @@ class DioClient { } } + Future post(String path, {dynamic data, Options? options}) async { + try { + final response = await _dio.post( + path, + data: data, + options: + options ?? Options(headers: {'Content-Type': 'application/json'}), + ); + return response; + } on DioException catch (e) { + print('DioError: ${e.response?.data ?? e.message}'); + rethrow; + } catch (e) { + print('Unexpected error: $e'); + rethrow; + } + } + Future updateUserProfile( String id, FormData formData, String token) async { try { @@ -268,7 +270,7 @@ class DioClient { ); print('getExercises response: ${response.data}'); return response; - } on DioError catch (e) { + } on DioException catch (e) { print( 'DioError: ${e.response?.statusCode} - ${e.response?.data ?? e.message}'); rethrow; @@ -336,4 +338,65 @@ class DioClient { rethrow; } } + + Future getCompletedTopics(String token) async { + try { + final response = await _dio.get( + '/topic/complete', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('getCompletedTopics response: ${response.data}'); + return response; + } catch (e) { + print('GetCompletedTopics error: $e'); + rethrow; + } + } + + Future studentFeedback( + String stdLearningId, + String feedback, + String token, + ) async { + try { + final response = await _dio.put( + '/stdLearning/$stdLearningId', + data: {'FEEDBACK_STUDENT': feedback}, + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + return response; + } catch (e) { + print('Submit Feedback error: $e'); + rethrow; + } + } + + Future getStudentAnswers(String stdLearningId, String token) async { + try { + final response = await _dio.get( + '/studentAnswer/$stdLearningId', + options: Options( + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ), + ); + print('studentAnswers response: ${response.data}'); + return response; + } catch (e) { + print('StudentAnswers error: $e'); + rethrow; + } + } } diff --git a/lib/core/services/local_storage_service.dart b/lib/core/services/local_storage_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/services/repositories/completed_topics_repository.dart b/lib/core/services/repositories/completed_topics_repository.dart new file mode 100644 index 0000000..ad489df --- /dev/null +++ b/lib/core/services/repositories/completed_topics_repository.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; +import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/home/models/completed_topics_model.dart'; + +class CompletedTopicsRepository { + final DioClient _dioClient; + + CompletedTopicsRepository(this._dioClient); + + Future> getCompletedTopics(String token) async { + try { + final response = await _dioClient.getCompletedTopics(token); + if (response.statusCode == 200) { + final List topicsData = response.data['payload']; + return topicsData.map((data) => CompletedTopic.fromJson(data)).toList(); + } else { + throw Exception( + 'Failed to load completed topics: ${response.statusMessage}'); + } + } on DioException catch (e) { + throw Exception('Network error: ${e.message}'); + } catch (e) { + throw Exception('Unexpected error: $e'); + } + } +} diff --git a/lib/core/services/repositories/constants.dart b/lib/core/services/repositories/constants.dart index a568a30..722458b 100644 --- a/lib/core/services/repositories/constants.dart +++ b/lib/core/services/repositories/constants.dart @@ -1 +1,2 @@ -const String baseUrl = 'https://3311-114-6-25-184.ngrok-free.app/'; +const String baseUrl = + 'https://70e7-2001-448a-50a0-2604-b558-2a22-54f6-65ce.ngrok-free.app/'; diff --git a/lib/core/services/repositories/exercise_repository.dart b/lib/core/services/repositories/exercise_repository.dart index f4f8dc4..8d030f2 100644 --- a/lib/core/services/repositories/exercise_repository.dart +++ b/lib/core/services/repositories/exercise_repository.dart @@ -1,4 +1,5 @@ import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart'; class ExerciseRepository { final DioClient _dioClient; @@ -19,6 +20,20 @@ class ExerciseRepository { } } + Future> getStudentAnswers( + String stdLearningId, String token) async { + try { + final response = await _dioClient.getStudentAnswers(stdLearningId, token); + if (response.statusCode == 200) { + return response.data['data']; + } else { + throw Exception('Failed to load student answers'); + } + } catch (e) { + throw Exception('Error fetching student answers: $e'); + } + } + Future> submitAnswersAndGetScore( List> answers, String studentLearningId, @@ -38,4 +53,25 @@ class ExerciseRepository { throw Exception('Error submitting answers and getting score: $e'); } } + + Future submitFeedback( + String stdLearningId, + String feedback, + String token, + ) async { + try { + final response = await _dioClient.studentFeedback( + stdLearningId, + feedback, + token, + ); + if (response.statusCode == 200) { + return FeedbackModel.fromJson(response.data['payload']); + } else { + throw Exception('Failed to submit feedback'); + } + } catch (e) { + throw Exception('Error submitting feedback: $e'); + } + } } diff --git a/lib/core/services/repositories/level_repository.dart b/lib/core/services/repositories/level_repository.dart index 2660057..5f1a6f6 100644 --- a/lib/core/services/repositories/level_repository.dart +++ b/lib/core/services/repositories/level_repository.dart @@ -10,12 +10,12 @@ class LevelRepository { if (response.statusCode == 200 && response.data != null) { final Map responseData = response.data; if (responseData.containsKey('data')) { - final Map data = responseData['data']; + final Map data = responseData['data'] ?? {}; final List levelsData = data['levels'] ?? []; final List levels = levelsData.map((json) => Level.fromJson(json)).toList(); final Map? lastCompletedLevel = - data['lastCompletedLevel']; + data['lastCompletedLevel'] ?? {}; return { 'levels': levels, @@ -32,4 +32,20 @@ class LevelRepository { rethrow; } } + + Future> getStudentAnswers( + String stdLearningId, String token) async { + try { + final response = await _dioClient.getStudentAnswers(stdLearningId, token); + + if (response.statusCode == 200 && response.data != null) { + return response.data['data']; + } else { + throw Exception( + 'Failed to fetch student answers: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Failed to fetch student answers: $e'); + } + } } diff --git a/lib/features/history/models/history_model.dart b/lib/features/history/models/history_model.dart index a23dfb6..7cfc985 100644 --- a/lib/features/history/models/history_model.dart +++ b/lib/features/history/models/history_model.dart @@ -2,9 +2,9 @@ import 'package:intl/intl.dart'; class LearningHistory { final int score; - final dynamic currentLevel; - final dynamic nextLevel; - final DateTime studentFinish; + final String currentLevel; + final String? nextLevel; + final DateTime? studentFinish; final String topicName; final String sectionName; @@ -19,16 +19,20 @@ class LearningHistory { factory LearningHistory.fromJson(Map json) { return LearningHistory( - score: json['SCORE'], - currentLevel: json['CURRENT_LEVEL'], + score: json['SCORE'] ?? 0, + currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown', nextLevel: json['NEXT_LEVEL'], - studentFinish: DateTime.parse(json['STUDENT_FINISH']), - topicName: json['TOPIC_NAME'], - sectionName: json['SECTION_NAME'], + studentFinish: json['STUDENT_FINISH'] != null + ? DateTime.parse(json['STUDENT_FINISH']) + : null, + topicName: json['TOPIC_NAME'] ?? 'Unknown Topic', + sectionName: json['SECTION_NAME'] ?? 'Unknown Section', ); } String get formattedDate { - return DateFormat('yyyy-MM-dd HH:mm').format(studentFinish); + return studentFinish != null + ? DateFormat('yyyy-MM-dd HH:mm').format(studentFinish!) + : 'N/A'; } } diff --git a/lib/features/history/provider/history_provider.dart b/lib/features/history/provider/history_provider.dart index ead5fea..569cc41 100644 --- a/lib/features/history/provider/history_provider.dart +++ b/lib/features/history/provider/history_provider.dart @@ -67,20 +67,50 @@ class HistoryProvider with ChangeNotifier { } } - int? _parseLevel(dynamic level) { - if (level is int) { - return level; - } else if (level is String) { - if (level.toLowerCase() == 'pretest') { - return 0; // Treat Pretest as level 0 - } - return int.tryParse(level.replaceAll('Level ', '')); + int? _parseLevel(String? level) { + if (level == null) return null; + if (level.toLowerCase() == 'pretest') { + return 0; // Treat Pretest as level 0 } - return null; + return int.tryParse(level.replaceAll('Level ', '')) ?? -1; } Future refreshData(String token) async { - await _sectionProvider.fetchSections(token); + if (_sectionProvider.sections.isEmpty) { + await _sectionProvider.fetchSections(token); + } await fetchLearningHistory(token); } + + Future loadHistoryData(String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + // Fetch sections and learning history in parallel + final sectionsResult = _sectionProvider.fetchSections(token); + + // Use the first section ID for initial history fetch + final firstSectionId = await sectionsResult + .then((sections) => sections.isNotEmpty ? sections.first.id : null); + + if (firstSectionId != null) { + final historyResult = + _repository.getLearningHistory(firstSectionId, token); + + // Wait for both futures to complete + final results = await Future.wait([sectionsResult, historyResult]); + + _learningHistory = results[1] as List; + } else { + _error = 'No sections available'; + } + } catch (e) { + _error = 'Error loading data: ${e.toString()}'; + } finally { + _isLoading = false; + notifyListeners(); + } + } } diff --git a/lib/features/history/screens/history_screen.dart b/lib/features/history/screens/history_screen.dart index 83e26b1..36c0162 100644 --- a/lib/features/history/screens/history_screen.dart +++ b/lib/features/history/screens/history_screen.dart @@ -1,3 +1,4 @@ +import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/history/provider/history_provider.dart'; @@ -18,95 +19,141 @@ class HistoryScreen extends StatefulWidget { } class _HistoryScreenState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final historyProvider = - Provider.of(context, listen: false); - final userProvider = Provider.of(context, listen: false); - - historyProvider.refreshData(userProvider.jwtToken!); - }); - }); + bool isNotFoundError(String error) { + return error.toLowerCase().contains('no learning history found') || + error.toLowerCase().contains('not found'); } + // @override + // void initState() { + // super.initState(); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // final historyProvider = + // Provider.of(context, listen: false); + // final userProvider = Provider.of(context, listen: false); + + // historyProvider.refreshData(userProvider.jwtToken!); + // }); + // }); + // } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgSoftColor, - body: SafeArea( - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 30.0, - ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Your Exercise History!', - style: AppTextStyles.blueTextStyle.copyWith( - fontSize: 18, - fontWeight: FontWeight.bold, + return Consumer( + builder: (context, historyProvider, chiild) { + return Scaffold( + backgroundColor: AppColors.bgSoftColor, + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 30.0, + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your Exercise History!', + style: AppTextStyles.blueTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 8), - Text( - 'Track your progress with a personalized overview of all your workouts.', - style: AppTextStyles.greyTextStyle.copyWith( - fontSize: 13, - fontWeight: FontWeight.w500, + const SizedBox(height: 8), + Text( + 'Track your progress with a personalized overview of all your workouts.', + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 8), - const CustomTabBar(), - const SizedBox(height: 8), - Expanded( - child: Consumer( - builder: (context, historyProvider, child) { - if (historyProvider.isLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (historyProvider.error != null) { - return Center(child: Text(historyProvider.error!)); - } else if (historyProvider.learningHistory.isEmpty) { - return _buildEmptyState(context); - } else { - return ListView.builder( - itemCount: historyProvider.learningHistory.length, - itemBuilder: (context, index) { - return Column( - children: [ - ExerciseHistoryCard( - exercise: - historyProvider.learningHistory[index], - ), - const SizedBox(height: 8.0), - ], - ); - }, - ); - } - }, + const SizedBox(height: 8), + const CustomTabBar(), + const SizedBox(height: 8), + Expanded( + child: Consumer( + builder: (context, historyProvider, child) { + if (historyProvider.isLoading) { + return const Center( + child: CircularProgressIndicator()); + } else if (historyProvider.error != null) { + if (isNotFoundError(historyProvider.error!)) { + return _buildEmptyState(context); + } else { + return _buildErrorState(historyProvider.error!); + } + } else if (historyProvider.learningHistory.isEmpty) { + return _buildEmptyState(context); + } else { + return ListView.builder( + itemCount: historyProvider.learningHistory.length, + itemBuilder: (context, index) { + return Column( + children: [ + ExerciseHistoryCard( + exercise: + historyProvider.learningHistory[index], + ), + const SizedBox(height: 8.0), + ], + ); + }, + ); + } + }, + ), ), - ), - ], + ], + ), ), ), ), + ); + }); + } + + Widget _buildErrorState(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + BootstrapIcons.exclamation_diamond_fill, + color: AppColors.blueColor, + size: 82, + ), + const SizedBox(height: 16), + Text( + 'Error: $error', + style: AppTextStyles.redTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + GlobalButton( + onPressed: () { + final historyProvider = + Provider.of(context, listen: false); + final userProvider = + Provider.of(context, listen: false); + historyProvider.refreshData(userProvider.jwtToken!); + }, + text: 'Try Again', + ), + ], ), ); } diff --git a/lib/features/history/widgets/custom_tab_bar.dart b/lib/features/history/widgets/custom_tab_bar.dart index 2f61a4b..a4fc9b6 100644 --- a/lib/features/history/widgets/custom_tab_bar.dart +++ b/lib/features/history/widgets/custom_tab_bar.dart @@ -5,21 +5,8 @@ import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:provider/provider.dart'; -class CustomTabBar extends StatefulWidget { - const CustomTabBar({super.key}); - - @override - _CustomTabBarState createState() => _CustomTabBarState(); -} - -class _CustomTabBarState extends State { - final ScrollController _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } +class CustomTabBar extends StatelessWidget { + const CustomTabBar({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -27,12 +14,7 @@ class _CustomTabBarState extends State { final sectionProvider = Provider.of(context); final selectedPageIndex = historyProvider.selectedPageIndex; - if (sectionProvider.sections.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - return SingleChildScrollView( - controller: _scrollController, scrollDirection: Axis.horizontal, child: Row( children: sectionProvider.sections.asMap().entries.map((entry) { @@ -56,12 +38,6 @@ class _CustomTabBarState extends State { Provider.of(context, listen: false); historyProvider.setSelectedPageIndex(index); - _scrollController.animateTo( - index * 100.0, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - final userProvider = Provider.of(context, listen: false); historyProvider.fetchLearningHistory(userProvider.jwtToken!); } diff --git a/lib/features/history/widgets/exercise_history_card.dart b/lib/features/history/widgets/exercise_history_card.dart index bb7c1af..dd971d4 100644 --- a/lib/features/history/widgets/exercise_history_card.dart +++ b/lib/features/history/widgets/exercise_history_card.dart @@ -28,84 +28,71 @@ class ExerciseHistoryCard extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 18.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - children: [ - TextSpan( - text: '${exercise.topicName} /', - ), - TextSpan( - text: ' ${exercise.sectionName}', - style: AppTextStyles.blackTextStyle) - ]), - ), - const SizedBox(height: 8), - RichText( - text: TextSpan( - style: AppTextStyles.greyTextStyle.copyWith( - fontSize: 12, - ), - children: [ - TextSpan( - text: '${exercise.currentLevel} → ', - style: AppTextStyles.blackTextStyle.copyWith( - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: '${exercise.nextLevel}', - style: AppTextStyles.blackTextStyle.copyWith( - color: color, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const SizedBox(height: 8), - Text( - 'Submission: ${exercise.formattedDate}', - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], + Text( + exercise.topicName, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.tetriaryTextStyle.copyWith( + fontWeight: FontWeight.w500, + ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 26.0, - ), - decoration: BoxDecoration( - border: Border.all( - color: color, + const SizedBox(height: 8), + RichText( + text: TextSpan( + style: AppTextStyles.greyTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, ), - borderRadius: BorderRadius.circular(8.0), + children: [ + TextSpan( + text: '${exercise.currentLevel} → ', + style: AppTextStyles.blackTextStyle.copyWith(), + ), + TextSpan( + text: '${exercise.nextLevel}', + style: AppTextStyles.blackTextStyle.copyWith( + color: color, + ), + ), + ], ), - child: Text( - '${exercise.score}/100', - style: TextStyle( - color: color, - fontWeight: FontWeight.bold, - ), + ), + const SizedBox(height: 8), + Text( + 'Submission: ${exercise.formattedDate}', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, ), ), ], ), ), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 26.0, + ), + decoration: BoxDecoration( + border: Border.all( + color: color, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + '${exercise.score}/100', + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ), ], ), ), diff --git a/lib/features/home/models/completed_topics_model.dart b/lib/features/home/models/completed_topics_model.dart new file mode 100644 index 0000000..b0d2eac --- /dev/null +++ b/lib/features/home/models/completed_topics_model.dart @@ -0,0 +1,28 @@ +class CompletedTopic { + final String idSection; + final String nameSection; + final String descriptionSection; + final String thumbnail; + final int totalTopics; + final int completedTopics; + + CompletedTopic({ + required this.idSection, + required this.nameSection, + required this.descriptionSection, + required this.thumbnail, + required this.totalTopics, + required this.completedTopics, + }); + + factory CompletedTopic.fromJson(Map json) { + return CompletedTopic( + idSection: json['ID_SECTION'], + nameSection: json['NAME_SECTION'], + descriptionSection: json['DESCRIPTION_SECTION'], + thumbnail: json['THUMBNAIL'], + totalTopics: json['TOTAL_TOPICS'], + completedTopics: json['COMPLETED_TOPICS'], + ); + } +} diff --git a/lib/features/home/provider/completed_topics_provider.dart b/lib/features/home/provider/completed_topics_provider.dart new file mode 100644 index 0000000..fbd9cd4 --- /dev/null +++ b/lib/features/home/provider/completed_topics_provider.dart @@ -0,0 +1,34 @@ +// lib/features/home/providers/completed_topics_provider.dart + +import 'package:english_learning/core/services/repositories/completed_topics_repository.dart'; +import 'package:english_learning/features/home/models/completed_topics_model.dart'; +import 'package:flutter/foundation.dart'; + +class CompletedTopicsProvider with ChangeNotifier { + final CompletedTopicsRepository _repository; + List _completedTopics = []; + bool _isLoading = false; + String? _error; + + CompletedTopicsProvider(this._repository); + + List get completedTopics => _completedTopics; + bool get isLoading => _isLoading; + String? get error => _error; + + Future fetchCompletedTopics(String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _completedTopics = await _repository.getCompletedTopics(token); + } catch (e) { + _error = e.toString(); + print('Error fetching completed topics: $_error'); + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 9cf2082..0ddc28c 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -1,8 +1,11 @@ import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:english_learning/core/widgets/custom_button.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart'; +import 'package:english_learning/features/history/provider/history_provider.dart'; import 'package:english_learning/features/history/screens/history_screen.dart'; import 'package:english_learning/features/home/data/card_data.dart'; +import 'package:english_learning/features/home/provider/completed_topics_provider.dart'; import 'package:english_learning/features/home/widgets/progress_card.dart'; import 'package:english_learning/features/home/widgets/welcome_card.dart'; import 'package:english_learning/features/learning/screens/learning_screen.dart'; @@ -14,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_nav_bar/google_nav_bar.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -78,15 +82,19 @@ class _HomeScreenState extends State { iconSize: 20, gap: 8, selectedIndex: _selectedIndex, - onTabChange: (index) { + onTabChange: (index) async { setState(() { _selectedIndex = index; - _pageController.animateToPage( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, // Animasi ketika berpindah tab - ); + _pageController.jumpToPage(index); }); + if (index == 2) { + // Index 2 adalah tab History + final historyProvider = + Provider.of(context, listen: false); + final userProvider = + Provider.of(context, listen: false); + await historyProvider.loadHistoryData(userProvider.jwtToken!); + } }, padding: const EdgeInsets.symmetric( horizontal: 16, @@ -127,164 +135,294 @@ class HomeContent extends StatefulWidget { class _HomeContentState extends State { final CardData cardData = CardData(); int _currentPage = 0; - bool hasOngoingExercises = false; + + @override + void initState() { + super.initState(); + // Memanggil fetchCompletedTopics saat HomeContent diinisialisasi + WidgetsBinding.instance.addPostFrameCallback((_) { + final userProvider = Provider.of(context, listen: false); + final completedTopicsProvider = + Provider.of(context, listen: false); + completedTopicsProvider.fetchCompletedTopics(userProvider.jwtToken!); + }); + } + + Widget _buildNoDataWidget() { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Still new?', + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Begin your journey!', + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 14, + ), + ), + const SizedBox(height: 24), + CustomButton( + text: 'Explore', + width: double.infinity, + height: 44, + color: AppColors.yellowButtonColor, + onPressed: () {}, + ), + ], + ), + ); + } + + Widget _buildShimmerEffect() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Column( + children: List.generate( + 2, + (index) => Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Container( + width: double.infinity, + height: 150, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + )), + ), + ), + ); + } @override Widget build(BuildContext context) { - return Consumer(builder: (context, authProvider, child) { + return Consumer2(builder: ( + context, + authProvider, + completedTopicsProvider, + child, + ) { final userName = authProvider.getUserName() ?? 'Guest'; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: AppColors.gradientTheme, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: AppColors.gradientTheme, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 60.0, - left: 18.34, - right: 16.0, - bottom: 34.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - 'lib/features/home/assets/images/Logo.svg', - width: 31, - ), - const SizedBox(width: 4.34), - Text( - 'SEALS', - style: AppTextStyles.logoTextStyle.copyWith( - fontSize: 28, - fontWeight: FontWeight.w700, + child: Padding( + padding: const EdgeInsets.only( + top: 60.0, + left: 18.34, + right: 16.0, + bottom: 34.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + 'lib/features/home/assets/images/Logo.svg', + width: 31, ), - ), - ], - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const EditProfileScreen(), + const SizedBox(width: 4.34), + Text( + 'SEALS', + style: + AppTextStyles.logoTextStyle.copyWith( + fontSize: 28, + fontWeight: FontWeight.w700, + ), ), - ); - }, - child: const Icon( - BootstrapIcons.person_circle, - size: 28, - color: AppColors.whiteColor, + ], ), - ), - ], - ), - const SizedBox(height: 17), - RichText( - text: TextSpan( - text: 'Hi, ', - style: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, - ), - children: [ - TextSpan( - text: userName, - style: AppTextStyles.yellowTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, - ), - ), - TextSpan( - text: '!', - style: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w700, - fontSize: 18, + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const EditProfileScreen(), + ), + ); + }, + child: const Icon( + BootstrapIcons.person_circle, + size: 28, + color: AppColors.whiteColor, ), ), ], ), + const SizedBox(height: 17), + RichText( + text: TextSpan( + text: 'Hi, ', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + children: [ + TextSpan( + text: userName, + style: + AppTextStyles.yellowTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + TextSpan( + text: '!', + style: + AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text( + 'Let\'s evolve together', + style: AppTextStyles.whiteTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + CarouselSlider.builder( + itemCount: cardData.cardData.length, + itemBuilder: (context, index, realIndex) { + return WelcomeCard(cardModel: cardData.cardData[index]); + }, + options: CarouselOptions( + height: 168, + viewportFraction: 0.9, + enlargeCenterPage: true, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 3), + autoPlayAnimationDuration: const Duration(milliseconds: 800), + autoPlayCurve: Curves.fastOutSlowIn, + onPageChanged: (index, reason) { + setState( + () { + _currentPage = index; + }, + ); + }, + ), + ), + const SizedBox(height: 16), + SliderWidget( + currentPage: _currentPage, + itemCount: cardData.cardData.length, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 24.0, + right: 24.0, + bottom: 47.0, + ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + const Icon( + BootstrapIcons.info_circle, + color: AppColors.tetriaryColor, + size: 16, ), - const SizedBox(height: 8), + const SizedBox(width: 8), Text( - 'Let\'s evolve together', - style: AppTextStyles.whiteTextStyle.copyWith( + 'Your Last Journey.', + style: AppTextStyles.tetriaryTextStyle.copyWith( fontSize: 12, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w800, ), ), ], ), ), - ), - ], + completedTopicsProvider.isLoading + ? _buildShimmerEffect() + : completedTopicsProvider.completedTopics.isEmpty + ? _buildNoDataWidget() + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + itemCount: 1, + itemBuilder: (context, index) { + return ProgressCard( + completedTopic: completedTopicsProvider + .completedTopics, // Kirim seluruh list + ); + }, + ), + ], + ), ), - ], - ), - const SizedBox(height: 16), - CarouselSlider.builder( - itemCount: cardData.cardData.length, - itemBuilder: (context, index, realIndex) { - return WelcomeCard(cardModel: cardData.cardData[index]); - }, - options: CarouselOptions( - height: 168, - viewportFraction: 0.9, - enlargeCenterPage: true, - autoPlay: true, - autoPlayInterval: const Duration(seconds: 3), - autoPlayAnimationDuration: const Duration(milliseconds: 800), - autoPlayCurve: Curves.fastOutSlowIn, - onPageChanged: (index, reason) { - setState( - () { - _currentPage = index; - }, - ); - }, ), - ), - const SizedBox(height: 16), - SliderWidget( - currentPage: _currentPage, - itemCount: cardData.cardData.length, - ), - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: ProgressCard(), - ), - // hasOngoingExercises - // ? const Padding( - // padding: EdgeInsets.symmetric(horizontal: 16.0), - // child: - // ProgressCard(), // Display progress card if exercises are completed - // ) - // : const Padding( - // padding: EdgeInsets.symmetric(horizontal: 16.0), - // child: - // ExploreCard(), // Display ExploreCard if no exercises are completed - // ), - ], + ], + ), ); }); } diff --git a/lib/features/home/widgets/no_progress_card.dart b/lib/features/home/widgets/no_progress_card.dart new file mode 100644 index 0000000..04e936b --- /dev/null +++ b/lib/features/home/widgets/no_progress_card.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:english_learning/core/utils/styles/theme.dart'; + +class NoProgressCard extends StatelessWidget { + const NoProgressCard({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.whiteColor, + ), + ); + } +} diff --git a/lib/features/home/widgets/progress_bar.dart b/lib/features/home/widgets/progress_bar.dart index b27b9bc..dc3e118 100644 --- a/lib/features/home/widgets/progress_bar.dart +++ b/lib/features/home/widgets/progress_bar.dart @@ -2,31 +2,34 @@ import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; class ProgressBar extends StatelessWidget { - final int currentProgress; - final int totalProgress; + final int completedTopics; + final int totalTopics; const ProgressBar({ super.key, - required this.currentProgress, - required this.totalProgress, + required this.completedTopics, + required this.totalTopics, }); @override Widget build(BuildContext context) { - final progress = totalProgress > 0 ? currentProgress / totalProgress : 0.0; + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + final progress = totalTopics > 0 ? completedTopics / totalTopics : 0.0; return LayoutBuilder( builder: (context, constraints) { - final barWidth = constraints.maxWidth - 40; + final barWidth = constraints.maxWidth - 30; - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox( - width: barWidth, + width: double.infinity, child: Container( height: 12, decoration: BoxDecoration( - color: Colors.grey.shade300, + color: Colors.grey.shade200, borderRadius: BorderRadius.circular(7), ), child: Stack( @@ -44,12 +47,25 @@ class ProgressBar extends StatelessWidget { ), ), ), - const Spacer(), - Text( - '$currentProgress/$totalProgress', - style: AppTextStyles.blueTextStyle.copyWith( - fontSize: 14, - fontWeight: FontWeight.w500, + // const Spacer(), + SizedBox( + height: screenHeight * 0.02, + ), + RichText( + text: TextSpan( + text: '$completedTopics/$totalTopics ', + style: AppTextStyles.blueTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), + children: [ + TextSpan( + text: 'Topics Completed', + style: AppTextStyles.blueTextStyle.copyWith( + fontWeight: FontWeight.w500, + ), + ) + ], ), ), ], diff --git a/lib/features/home/widgets/progress_card.dart b/lib/features/home/widgets/progress_card.dart index 98fff04..7607ca1 100644 --- a/lib/features/home/widgets/progress_card.dart +++ b/lib/features/home/widgets/progress_card.dart @@ -1,91 +1,103 @@ import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:english_learning/core/services/repositories/constants.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/home/models/completed_topics_model.dart'; import 'package:english_learning/features/home/widgets/progress_bar.dart'; import 'package:flutter/material.dart'; class ProgressCard extends StatelessWidget { - const ProgressCard({super.key}); + final List completedTopic; + + const ProgressCard({super.key, required this.completedTopic}); + + String _getFullImageUrl(String thumbnail) { + return thumbnail.startsWith('http') + ? thumbnail + : '${baseUrl}uploads/section/$thumbnail'; + } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 16.0, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - const Icon( - BootstrapIcons.info_circle, - color: AppColors.disableColor, - size: 16, - ), - const SizedBox(width: 8), - Text( - 'Your Last Journey!', - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 12, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - const SizedBox(height: 24), - Container( - width: double.infinity, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppColors.disableColor, - ), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Listening', - style: AppTextStyles.blackTextStyle.copyWith( - fontSize: 15, - fontWeight: FontWeight.w900, - ), - ), - const SizedBox(height: 4), - Text( - 'Topic 8: Entertaining | Level 3', - style: AppTextStyles.disableTextStyle.copyWith( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 12), - const ProgressBar( - currentProgress: 8, - totalProgress: 11, - ), - ], - ), - ), - ), - ], - ), - ), - ) + ...completedTopic.asMap().entries.map( + (entry) { + CompletedTopic topic = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildTopicItem(topic), + ); + }, + ), ], ); } + + Widget _buildTopicItem(CompletedTopic topic) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + _getFullImageUrl(topic.thumbnail), + width: 90, + height: 130, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 90, + height: 130, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + topic.nameSection, + style: AppTextStyles.blackTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 4), + Text( + topic.descriptionSection, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.disableTextStyle.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + ProgressBar( + completedTopics: topic.completedTopics, + totalTopics: topic.totalTopics, + ), + ], + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/features/learning/modules/exercises/models/exercise_model.dart b/lib/features/learning/modules/exercises/models/exercise_model.dart index 0f1ead9..3172bcd 100644 --- a/lib/features/learning/modules/exercises/models/exercise_model.dart +++ b/lib/features/learning/modules/exercises/models/exercise_model.dart @@ -9,6 +9,8 @@ class ExerciseModel { final String? image; final DateTime timeAdminExc; final dynamic choices; + final String? answerStudent; + final bool? isCorrect; ExerciseModel({ required this.idAdminExercise, @@ -21,6 +23,8 @@ class ExerciseModel { this.image, required this.timeAdminExc, required this.choices, + this.answerStudent, + this.isCorrect, }); factory ExerciseModel.fromJson(Map json) { @@ -57,6 +61,21 @@ class ExerciseModel { choices: choices, ); } + + factory ExerciseModel.fromReviewJson(Map json) { + final exerciseDetails = json['exerciseDetails']; + return ExerciseModel( + idAdminExercise: exerciseDetails['ID_ADMIN_EXERCISE'], + idLevel: json['ID_LEVEL'] ?? '', // This might be available in parent data + title: exerciseDetails['TITLE'], + question: exerciseDetails['QUESTION'], + questionType: exerciseDetails['QUESTION_TYPE'], + timeAdminExc: DateTime.parse(json['TIME_STUDENT_EXC']), + answerStudent: json['ANSWER_STUDENT'], + isCorrect: json['IS_CORRECT'] == 1, + choices: null, + ); + } } class MultipleChoice { diff --git a/lib/features/learning/modules/exercises/providers/exercise_provider.dart b/lib/features/learning/modules/exercises/providers/exercise_provider.dart index 4a08c7f..9a01935 100644 --- a/lib/features/learning/modules/exercises/providers/exercise_provider.dart +++ b/lib/features/learning/modules/exercises/providers/exercise_provider.dart @@ -1,3 +1,4 @@ +import 'package:english_learning/features/learning/modules/feedback/models/feedback_model.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart'; @@ -23,6 +24,7 @@ class ExerciseProvider extends ChangeNotifier { String _nameLevel = ''; String? _activeLeftOption; String? _studentLearningId; + bool _isReview = false; // Constants final List _pairColors = [ @@ -42,6 +44,7 @@ class ExerciseProvider extends ChangeNotifier { String get nameLevel => _nameLevel; String? get activeLeftOption => _activeLeftOption; String? get studentLearningId => _studentLearningId; + bool get isReview => _isReview; // Initialization methods void initializeAnswers() { @@ -64,11 +67,40 @@ class ExerciseProvider extends ChangeNotifier { }); } - // Answer handling methods + // // Answer handling methods + // void answerQuestion(int index, String answer) { + // if (index >= 0 && index < _answers.length) { + // if (_exercises[index].choices is MatchingPair) { + // _handleMatchingPairAnswer(index, answer); + // } else { + // _answers[index] = _answers[index] == answer ? '' : answer; + // } + // notifyListeners(); + // } + // } void answerQuestion(int index, String answer) { + if (_isReview) return; if (index >= 0 && index < _answers.length) { if (_exercises[index].choices is MatchingPair) { _handleMatchingPairAnswer(index, answer); + } else if (_exercises[index].choices is MultipleChoice) { + // Store the letter index (e.g., "A", "B", "C", etc.) + final multipleChoice = _exercises[index].choices as MultipleChoice; + final options = [ + multipleChoice.optionA, + multipleChoice.optionB, + multipleChoice.optionC, + multipleChoice.optionD, + multipleChoice.optionE, + ]; + final optionIndex = options.indexOf(answer); + if (optionIndex != -1) { + _answers[index] = String.fromCharCode(65 + optionIndex); + } + } else if (_exercises[index].choices is TrueFalse) { + // Store "1" for true and "0" for false + // _answers[index] = answer.toLowerCase() == 'true' ? '1' : '0'; + _answers[index] = answer; } else { _answers[index] = _answers[index] == answer ? '' : answer; } @@ -238,6 +270,33 @@ class ExerciseProvider extends ChangeNotifier { } } + Future fetchReviewExercises(String stdLearningId) async { + _isLoading = true; + _isReview = true; + notifyListeners(); + + try { + final token = await _userProvider.getValidToken(); + if (token == null) { + throw Exception('No valid token found'); + } + + final data = await _repository.getStudentAnswers(stdLearningId, token); + _nameTopic = data['NAME_TOPIC']; + _nameLevel = data['NAME_LEVEL']; + final exercisesData = data['stdExercises']; + _exercises = exercisesData + .map((json) => ExerciseModel.fromReviewJson(json)) + .toList(); + _answers = _exercises.map((e) => e.answerStudent ?? '').toList(); + } catch (e) { + print('Error fetching review exercises: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + Future> submitAnswersAndGetScore() async { print('submitAnswersAndGetScore called'); try { @@ -256,10 +315,17 @@ class ExerciseProvider extends ChangeNotifier { }).map((entry) { final index = entry.key; final exercise = entry.value; + String formattedAnswer = _answers[index]; + if (exercise.choices is MultipleChoice) { + formattedAnswer = formattedAnswer; + } else if (exercise.choices is TrueFalse) { + formattedAnswer = formattedAnswer.toLowerCase() == 'true' ? '1' : '0'; + } + return { 'ID_STUDENT_LEARNING': _studentLearningId!, 'ID_ADMIN_EXERCISE': exercise.idAdminExercise, - 'ANSWER_STUDENT': _answers[index], + 'ANSWER_STUDENT': formattedAnswer, }; }).toList(); @@ -286,6 +352,26 @@ class ExerciseProvider extends ChangeNotifier { } } + Future submitFeedback(String feedback) async { + try { + final token = await _userProvider.getValidToken(); + if (token == null) { + throw Exception('No valid token found'); + } + if (_studentLearningId == null) { + throw Exception('Student Learning ID is not set'); + } + print('Submitting feedback for stdLearningId: $_studentLearningId'); + final result = await _repository.submitFeedback( + _studentLearningId!, feedback, token); + print('Feedback submitted successfully'); + return result; + } catch (e) { + print('Error submitting feedback: $e'); + rethrow; + } + } + bool hasAnsweredQuestions() { return _answers.any((answer) => answer.isNotEmpty); } diff --git a/lib/features/learning/modules/exercises/screens/exercise_screen.dart b/lib/features/learning/modules/exercises/screens/exercise_screen.dart index bd79e0c..966db74 100644 --- a/lib/features/learning/modules/exercises/screens/exercise_screen.dart +++ b/lib/features/learning/modules/exercises/screens/exercise_screen.dart @@ -10,11 +10,13 @@ import 'package:provider/provider.dart'; class ExerciseScreen extends StatefulWidget { final String? levelId; final String studentLearningId; + final bool isReview; const ExerciseScreen({ super.key, required this.levelId, required this.studentLearningId, + this.isReview = false, }); @override @@ -29,7 +31,11 @@ class _ExerciseScreenState extends State { _scrollController = ScrollController(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); - provider.fetchExercises(widget.levelId!); + if (widget.isReview) { + provider.fetchReviewExercises(widget.studentLearningId); + } else { + provider.fetchExercises(widget.levelId!); + } provider.setStudentLearningId(widget.studentLearningId); }); } @@ -98,6 +104,8 @@ class _ExerciseScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ + if (provider.isReview) + Text('Review Mode', style: TextStyle(fontSize: 24)), const SizedBox(height: 16), const ExerciseContent(), const SizedBox(height: 24), diff --git a/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart b/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart index da312f7..0c22a7a 100644 --- a/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart +++ b/lib/features/learning/modules/exercises/widgets/exercise_navigator.dart @@ -31,10 +31,11 @@ class ExerciseNavigator extends StatelessWidget { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => ResultScreen( - currentLevel: result['CURRENT_LEVEL_NAME'], - nextLevel: result['NEXT_LEARNING_NAME'], - score: int.parse(result['SCORE'].toString()), + currentLevel: result['CURRENT_LEVEL_NAME'] ?? '', + nextLevel: result['NEXT_LEARNING_NAME'] ?? '', + score: int.tryParse(result['SCORE'].toString()) ?? 0, isCompleted: result['IS_PASS'] == 1, + stdLearningId: result['STUDENT_LEARNING_ID']?.toString(), ), ), ); diff --git a/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart b/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart index 0dae68d..88e0725 100644 --- a/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/multiple_choice_question.dart @@ -1,4 +1,3 @@ -// multiple_choice_question.dart import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; @@ -31,7 +30,8 @@ class MultipleChoiceQuestion extends StatelessWidget { Widget _buildOptionsList(List options, ExerciseProvider provider) { final optionLabels = List.generate( options.length, - (index) => String.fromCharCode(65 + index), + (index) => + String.fromCharCode(65 + index), // Generate labels "A", "B", etc. ); return ListView.builder( @@ -41,7 +41,7 @@ class MultipleChoiceQuestion extends StatelessWidget { itemBuilder: (context, i) { final option = options[i]; final isSelected = - provider.answers[provider.currentExerciseIndex] == option; + provider.answers[provider.currentExerciseIndex] == optionLabels[i]; return GestureDetector( onTap: () => @@ -69,10 +69,8 @@ class MultipleChoiceQuestion extends StatelessWidget { ), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 10.0, - ), + padding: + const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), child: Text( label, style: AppTextStyles.blackTextStyle.copyWith( diff --git a/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart b/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart index 62451c2..34bbba4 100644 --- a/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart +++ b/lib/features/learning/modules/exercises/widgets/question/true_false_question.dart @@ -28,12 +28,15 @@ class TrueFalseQuestion extends StatelessWidget { itemCount: options.length, itemBuilder: (context, i) { final option = options[i]; - final isSelected = - provider.answers[provider.currentExerciseIndex] == option; + final isSelected = provider.answers[provider.currentExerciseIndex] == + (i == 0 ? '1' : '0'); return GestureDetector( - onTap: () => - provider.answerQuestion(provider.currentExerciseIndex, option), + // onTap: () => + // provider.answerQuestion(provider.currentExerciseIndex, option), + onTap: () => provider.answerQuestion( + provider.currentExerciseIndex, i == 0 ? '1' : '0'), + child: _buildOptionItem(option, isSelected), ); }, diff --git a/lib/features/learning/modules/feedback/models/feedback_model.dart b/lib/features/learning/modules/feedback/models/feedback_model.dart new file mode 100644 index 0000000..f0324f6 --- /dev/null +++ b/lib/features/learning/modules/feedback/models/feedback_model.dart @@ -0,0 +1,40 @@ +class FeedbackModel { + final String idStudentLearning; + final String id; + final String idLevel; + final DateTime studentStart; + final DateTime studentFinish; + final int score; + final bool isPass; + final String nextLearning; + final String feedbackStudent; + final DateTime timeLearning; + + FeedbackModel({ + required this.idStudentLearning, + required this.id, + required this.idLevel, + required this.studentStart, + required this.studentFinish, + required this.score, + required this.isPass, + required this.nextLearning, + required this.feedbackStudent, + required this.timeLearning, + }); + + factory FeedbackModel.fromJson(Map json) { + return FeedbackModel( + idStudentLearning: json['ID_STUDENT_LEARNING'], + id: json['ID'], + idLevel: json['ID_LEVEL'], + studentStart: DateTime.parse(json['STUDENT_START']), + studentFinish: DateTime.parse(json['STUDENT_FINISH']), + score: json['SCORE'], + isPass: json['IS_PASS'] == 1, + nextLearning: json['NEXT_LEARNING'], + feedbackStudent: json['FEEDBACK_STUDENT'], + timeLearning: DateTime.parse(json['TIME_LEARNING']), + ); + } +} diff --git a/lib/features/learning/modules/feedback/providers/feedback_provider.dart b/lib/features/learning/modules/feedback/providers/feedback_provider.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/learning/modules/feedback/screens/feedback_screen.dart b/lib/features/learning/modules/feedback/screens/feedback_screen.dart index 0e7961b..435a486 100644 --- a/lib/features/learning/modules/feedback/screens/feedback_screen.dart +++ b/lib/features/learning/modules/feedback/screens/feedback_screen.dart @@ -1,10 +1,17 @@ import 'package:english_learning/core/widgets/global_button.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; +import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; import 'package:english_learning/features/learning/modules/feedback/widgets/feedback_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class FeedbackScreen extends StatefulWidget { - const FeedbackScreen({super.key}); + final String stdLearningId; + + const FeedbackScreen({ + super.key, + required this.stdLearningId, + }); @override State createState() => _FeedbackScreenState(); @@ -33,6 +40,58 @@ class _FeedbackScreenState extends State { _focusNode.unfocus(); } + Future _submitFeedback() async { + if (_controller.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter your feedback before submitting.'), + backgroundColor: Colors.red, + ), + ); + return; + } + + final exerciseProvider = + Provider.of(context, listen: false); + + try { + final result = await exerciseProvider.submitFeedback(_controller.text); + + print('Feedback submitted successfully: $result'); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Feedback submitted successfully!'), + backgroundColor: Colors.green, + ), + ); + + // Show the dialog + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return FeedbackDialog( + onSubmit: () { + Navigator.of(dialogContext).pop(); // Close the dialog + Navigator.of(context).pop(); // Return to the previous screen + }, + ); + }, + ); + } + } catch (e) { + print('Error submitting feedback: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to submit feedback: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -121,23 +180,7 @@ class _FeedbackScreenState extends State { const SizedBox(height: 24), GlobalButton( text: 'Send', - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return FeedbackDialog( - onSubmit: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => const LevelListScreen(), - // ), - // ); - }, - ); - }, - ); - }, + onPressed: _submitFeedback, ), const SizedBox(height: 16), GlobalButton( @@ -149,9 +192,13 @@ class _FeedbackScreenState extends State { // Navigator.push( // context, // MaterialPageRoute( - // builder: (context) => const LevelListScreen(), + // builder: (context) => LevelListScreen( + // topicId: widget.topicId!, + // topicTitle: widget.topicTitle!, + // ), // ), // ); + Navigator.of(context).pop(true); }, ) ], diff --git a/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart b/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart index 9607c30..cc33e16 100644 --- a/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart +++ b/lib/features/learning/modules/feedback/widgets/feedback_dialog.dart @@ -58,6 +58,11 @@ class FeedbackDialog extends StatelessWidget { // builder: (context) => const LevelListScreen(), // ), // ); + onSubmit(); + // Navigator.of(context).popUntil( + // (route) => route.isFirst, + // ); + Navigator.of(context).pop(true); }, ) ], diff --git a/lib/features/learning/modules/level/models/level_model.dart b/lib/features/learning/modules/level/models/level_model.dart index 372b5b9..2bbabdc 100644 --- a/lib/features/learning/modules/level/models/level_model.dart +++ b/lib/features/learning/modules/level/models/level_model.dart @@ -33,17 +33,17 @@ class Level { factory Level.fromJson(Map json) { return Level( - idLevel: json['ID_LEVEL'], - idTopic: json['ID_TOPIC'], - idSection: json['ID_SECTION'], - nameSection: json['NAME_SECTION'], - nameTopic: json['NAME_TOPIC'], - nameLevel: json['NAME_LEVEL'], - content: json['CONTENT'], + idLevel: json['ID_LEVEL'] ?? '', + idTopic: json['ID_TOPIC'] ?? '', + idSection: json['ID_SECTION'] ?? '', + nameSection: json['NAME_SECTION'] ?? '', + nameTopic: json['NAME_TOPIC'] ?? '', + nameLevel: json['NAME_LEVEL'] ?? '', + content: json['CONTENT'] ?? '', audio: json['AUDIO'], image: json['IMAGE'], video: json['VIDEO'], - isPretest: json['IS_PRETEST'], + isPretest: json['IS_PRETEST'] ?? 0, timeLevel: json['TIME_LEVEL'], idStudentLearning: json['ID_STUDENT_LEARNING'], score: json['SCORE'], diff --git a/lib/features/learning/modules/level/providers/level_provider.dart b/lib/features/learning/modules/level/providers/level_provider.dart index c079861..8014d65 100644 --- a/lib/features/learning/modules/level/providers/level_provider.dart +++ b/lib/features/learning/modules/level/providers/level_provider.dart @@ -6,13 +6,17 @@ class LevelProvider with ChangeNotifier { final LevelRepository _levelRepository = LevelRepository(); List _levels = []; Map? _lastCompletedLevel; + List _unlockedLevels = []; bool _isLoading = false; String? _error; + Map? _studentAnswers; List get levels => _levels; Map? get lastCompletedLevel => _lastCompletedLevel; + List get unlockedLevels => _unlockedLevels; bool get isLoading => _isLoading; String? get error => _error; + Map? get studentAnswers => _studentAnswers; Future fetchLevels(String topicId, String token) async { _isLoading = true; @@ -23,6 +27,9 @@ class LevelProvider with ChangeNotifier { final result = await _levelRepository.getLevels(topicId, token); _levels = result['levels']; _lastCompletedLevel = result['lastCompletedLevel']; + _unlockedLevels = + List.from(_lastCompletedLevel?['UNLOCKED_LEVELS'] ?? []); + if (_levels.isEmpty) { _error = 'No levels found for this topic'; } @@ -39,37 +46,90 @@ class LevelProvider with ChangeNotifier { orElse: () => throw Exception('Pretest not found')); } + // int getLevelScore(String levelId) { + // final level = _levels.firstWhere((level) => level.idLevel == levelId, + // orElse: () => const Level( + // idLevel: '', + // idTopic: '', + // idSection: '', + // nameSection: '', + // nameTopic: '', + // nameLevel: '', + // content: '', + // isPretest: 0, + // timeLevel: '', + // )); + // return level.score ?? 0; + // } + + // bool isLevelAllowed(String levelName) { + // return _unlockedLevels.contains(levelName); + // } + + // bool isLevelCompleted(String levelId) { + // return _lastCompletedLevel != null && + // _lastCompletedLevel!['ID_LEVEL'] == levelId; + // } + + Future fetchStudentAnswers(String stdLearningId, String token) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _studentAnswers = + await _levelRepository.getStudentAnswers(stdLearningId, token); + } catch (e) { + _error = 'Error fetching student answers: ${e.toString()}'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + bool isPretestFinished(String levelId) { + return _levels.any( + (level) => level.idLevel == levelId && level.idStudentLearning != null); + } + + int getPretestScore(String levelId) { + final pretest = _levels.firstWhere( + (level) => level.idLevel == levelId && level.isPretest == 1, + orElse: () => throw Exception('Null')); + return pretest.score ?? 0; + } + int getLevelScore(String levelId) { final level = _levels.firstWhere((level) => level.idLevel == levelId, - orElse: () => const Level( - idLevel: '', - idTopic: '', - idSection: '', - nameSection: '', - nameTopic: '', - nameLevel: '', - content: '', - isPretest: 0, - timeLevel: '', - )); + orElse: () => throw Exception('Null')); return level.score ?? 0; } bool isLevelAllowed(String levelId) { - if (_lastCompletedLevel == null) { - // Jika tidak ada level yang selesai, hanya pretest yang diizinkan - return levelId == _levels.first.idLevel; - } - - String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL']; - int lastCompletedIndex = - _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId); - int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId); - - // Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1 - return currentIndex <= lastCompletedIndex + 1; + return _unlockedLevels.contains( + _levels.firstWhere((level) => level.idLevel == levelId).nameLevel); } + bool isLevelCompleted(String levelId) { + return _levels.any( + (level) => level.idLevel == levelId && level.idStudentLearning != null); + } + + // bool isLevelAllowed(String levelId) { + // if (_lastCompletedLevel == null) { + // // Jika tidak ada level yang selesai, hanya pretest yang diizinkan + // return levelId == _levels.first.idLevel; + // } + + // String lastCompletedLevelId = _lastCompletedLevel!['ID_LEVEL']; + // int lastCompletedIndex = + // _levels.indexWhere((level) => level.idLevel == lastCompletedLevelId); + // int currentIndex = _levels.indexWhere((level) => level.idLevel == levelId); + + // // Level diizinkan jika indeksnya kurang dari atau sama dengan indeks terakhir yang selesai + 1 + // return currentIndex <= lastCompletedIndex + 1; + // } + // bool isLevelAllowed(int levelIndex) { // if (levelIndex == 0) return true; // Pretest is always allowed // if (_lastCompletedLevel == null) diff --git a/lib/features/learning/modules/level/screens/level_list_screen.dart b/lib/features/learning/modules/level/screens/level_list_screen.dart index 9037b83..f215fa9 100644 --- a/lib/features/learning/modules/level/screens/level_list_screen.dart +++ b/lib/features/learning/modules/level/screens/level_list_screen.dart @@ -24,11 +24,13 @@ class _LevelListScreenState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - final levelProvider = Provider.of(context, listen: false); - final userProvider = Provider.of(context, listen: false); - levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!); - }); + _fetchLevels(); + } + + Future _fetchLevels() async { + final levelProvider = Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + await levelProvider.fetchLevels(widget.topicId, userProvider.jwtToken!); } @override @@ -69,16 +71,23 @@ class _LevelListScreenState extends State { if (levelProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } else if (levelProvider.error != null) { - return Center(child: Text('No levels available')); + return const Center(child: Text('No levels available')); } else { + final pretest = levelProvider.getPretest(); + final otherLevels = levelProvider.levels + .where((level) => level.isPretest == 0) + .toList(); + return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ PretestCard( - pretest: levelProvider.getPretest(), - score: levelProvider - .getLevelScore(levelProvider.getPretest().idLevel), + pretest: pretest, + score: levelProvider.getPretestScore(pretest.idLevel), + isCompleted: + levelProvider.isPretestFinished(pretest.idLevel), + isAllowed: levelProvider.isLevelAllowed(pretest.idLevel), ), const SizedBox(height: 12), Expanded( @@ -90,14 +99,17 @@ class _LevelListScreenState extends State { crossAxisSpacing: 16, mainAxisSpacing: 16, ), - itemCount: levelProvider.levels.length - 1, + itemCount: otherLevels.length, itemBuilder: (context, index) { - final level = levelProvider.levels[index + 1]; + final level = otherLevels[index]; + return LevelCard( level: level, isAllowed: levelProvider.isLevelAllowed(level.idLevel), score: levelProvider.getLevelScore(level.idLevel), + isCompleted: + levelProvider.isLevelCompleted(level.idLevel), ); }, ), diff --git a/lib/features/learning/modules/level/widgets/level_card.dart b/lib/features/learning/modules/level/widgets/level_card.dart index 7a604f1..83e2e6d 100644 --- a/lib/features/learning/modules/level/widgets/level_card.dart +++ b/lib/features/learning/modules/level/widgets/level_card.dart @@ -7,25 +7,19 @@ import 'package:flutter/material.dart'; class LevelCard extends StatelessWidget { final Level level; final bool isAllowed; - // final int level; - // final bool isAllowed; - // final bool isFinished; final int score; - // final VoidCallback? onPressed; + final bool isCompleted; + const LevelCard({ super.key, required this.level, required this.isAllowed, - // required this.level, - // required this.isAllowed, - // required this.isFinished, required this.score, - // this.onPressed, + required this.isCompleted, }); @override Widget build(BuildContext context) { - bool isCompleted = level.idStudentLearning != null; return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -128,14 +122,18 @@ class LevelCard extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: CustomButton( - text: isAllowed ? 'Learn Now' : 'Not Allowed', + text: isCompleted + ? 'Finished' + : (isAllowed ? 'Learn Now' : 'Locked'), textStyle: isAllowed ? null : AppTextStyles.disableTextStyle, width: double.infinity, height: 36, - color: isAllowed - ? AppColors.yellowButtonColor - : AppColors.cardButtonColor, + color: isCompleted + ? Colors.green + : (isAllowed + ? AppColors.yellowButtonColor + : AppColors.cardButtonColor), onPressed: isAllowed ? () { Navigator.push( diff --git a/lib/features/learning/modules/level/widgets/pretest_card.dart b/lib/features/learning/modules/level/widgets/pretest_card.dart index 027e9ce..1bb8112 100644 --- a/lib/features/learning/modules/level/widgets/pretest_card.dart +++ b/lib/features/learning/modules/level/widgets/pretest_card.dart @@ -2,124 +2,160 @@ import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/widgets/custom_button.dart'; import 'package:english_learning/features/learning/modules/level/models/level_model.dart'; +import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; import 'package:english_learning/features/learning/modules/material/screens/material_screen.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class PretestCard extends StatelessWidget { final Level pretest; final int? score; - final VoidCallback? onPressed; + final bool isCompleted; + final bool isAllowed; const PretestCard({ super.key, required this.pretest, this.score, - this.onPressed, + required this.isCompleted, + required this.isAllowed, }); @override Widget build(BuildContext context) { - bool isCompleted = pretest.idStudentLearning != null; + return Consumer(builder: (context, levelProvider, _) { + final isFinished = levelProvider.isPretestFinished(pretest.idLevel); + final score = levelProvider.getPretestScore(pretest.idLevel); + // final isAllowed = levelProvider.isLevelAllowed(pretest.idLevel); - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( + return Card( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - gradient: AppColors.gradientTheme, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: isCompleted ? Colors.green : Colors.transparent, - border: Border.all( - color: AppColors.whiteColor, - width: 1.5, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: AppColors.gradientTheme, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: + isCompleted ? Colors.green : Colors.transparent, + border: Border.all( + color: AppColors.whiteColor, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + pretest.nameLevel, + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 16, + ), + ), + if (isCompleted) ...[ + const SizedBox(width: 4), + const Icon( + BootstrapIcons.check_all, + color: AppColors.whiteColor, + size: 20, + ), + ], + ], ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - pretest.nameLevel, - style: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w900, - fontSize: 16, - ), - ), - if (isCompleted) ...[ - const SizedBox(width: 4), - const Icon( - BootstrapIcons.check_all, - color: AppColors.whiteColor, - size: 20, - ), - ], - ], + const SizedBox(height: 6), + Text( + 'Score $score/100', + style: AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + fontSize: 12, + ), ), - ), - const SizedBox(height: 6), - Text( - 'Score $score/100', - style: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w900, - fontSize: 12, - ), - ), - const SizedBox(height: 16), - ], - ), - Flexible( - child: Image.asset( - 'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png', - height: 95, - fit: BoxFit.cover, + const SizedBox(height: 16), + ], ), - ), - ], - ), - const SizedBox(height: 13), - CustomButton( - text: isCompleted ? 'Finished' : 'Learn Now', - textStyle: AppTextStyles.whiteTextStyle.copyWith( - fontWeight: FontWeight.w900, + Flexible( + child: Image.asset( + 'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png', + height: 95, + fit: BoxFit.cover, + ), + ), + ], ), - width: double.infinity, - height: 36, - color: isCompleted ? Colors.green : AppColors.yellowButtonColor, - onPressed: () { - if (!isCompleted) { + const SizedBox(height: 13), + CustomButton( + text: isFinished ? 'Review' : 'Learn Now', + textStyle: isFinished + ? AppTextStyles.whiteTextStyle.copyWith( + fontWeight: FontWeight.w900, + ) + : AppTextStyles.blackButtonTextStyle.copyWith( + fontWeight: FontWeight.w900, + ), + width: double.infinity, + height: 36, + color: isFinished ? Colors.green : AppColors.yellowButtonColor, + // onPressed: () { + // if (!isCompleted) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => MaterialScreen( + // levelId: pretest.idLevel, + // ), + // ), + // ); + // } + // // Jika isCompleted true, tidak melakukan apa-apa + // }, + // onPressed: isAllowed + // ? () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => MaterialScreen( + // levelId: pretest.idLevel, + // ), + // ), + // ); + // } + // : () {}, + onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => MaterialScreen( levelId: pretest.idLevel, + isReview: isFinished, ), ), ); - } - // Jika isCompleted true, tidak melakukan apa-apa - }, - ), - ], + }, + ), + ], + ), ), - ), - ); + ); + }); } } diff --git a/lib/features/learning/modules/material/screens/material_screen.dart b/lib/features/learning/modules/material/screens/material_screen.dart index 6d47798..2ebe2c2 100644 --- a/lib/features/learning/modules/material/screens/material_screen.dart +++ b/lib/features/learning/modules/material/screens/material_screen.dart @@ -14,9 +14,12 @@ import 'package:provider/provider.dart'; class MaterialScreen extends StatefulWidget { final String levelId; + final bool isReview; + const MaterialScreen({ super.key, required this.levelId, + this.isReview = false, }); @override @@ -74,7 +77,7 @@ class _MaterialScreenState extends State print('Student Learning created: ${result['message']}'); // Navigate to ExerciseScreen - Navigator.push( + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => ExerciseScreen( @@ -164,15 +167,28 @@ class _MaterialScreenState extends State videoUrl: level.video!, ), const SizedBox(height: 32), - GlobalButton( - text: 'Take Pretest', - onPressed: _isLoading - ? null - : () { - _stopAndResetAllMedia(); - _createStudentLearning(); - }, - ) + if (!widget.isReview) + GlobalButton( + text: level.isPretest == 1 + ? 'Take Pretest' + : 'Take Exercises', + onPressed: _isLoading + ? null + : () { + _stopAndResetAllMedia(); + _createStudentLearning(); + }, + ) + else + GlobalButton( + text: 'Start Review', + onPressed: _isLoading + ? null + : () { + _stopAndResetAllMedia(); + _createStudentLearning(); + }, + ), ], ), ), diff --git a/lib/features/learning/modules/material/widgets/video_player_widget.dart b/lib/features/learning/modules/material/widgets/video_player_widget.dart index 37669da..290ed05 100644 --- a/lib/features/learning/modules/material/widgets/video_player_widget.dart +++ b/lib/features/learning/modules/material/widgets/video_player_widget.dart @@ -20,6 +20,8 @@ class VideoPlayerWidgetState extends State { VideoPlayerController? _videoController; YoutubePlayerController? _youtubeController; FlickManager? _flickManager; + bool _isLoading = true; + String? _error; bool get wantKeepAlive => true; @@ -29,6 +31,18 @@ class VideoPlayerWidgetState extends State { _initializeVideoPlayerWidget(); } + String _getPlayableUrl(String url) { + if (url.contains('drive.google.com')) { + final regex = RegExp(r'/d/([a-zA-Z0-9-_]+)'); + final match = regex.firstMatch(url); + if (match != null) { + final fileId = match.group(1); + return 'https://drive.google.com/uc?export=download&id=$fileId'; + } + } + return url; + } + void _initializeVideoPlayerWidget() { if (YoutubePlayer.convertUrlToId(widget.videoUrl) != null) { _youtubeController = YoutubePlayerController( @@ -44,18 +58,32 @@ class VideoPlayerWidgetState extends State { showVideoProgressIndicator: true, onReady: () { _youtubeController!.addListener(_youtubeListener); + setState(() { + _isLoading = false; + }); }, ); } else { - _videoController = - VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); - _flickManager = FlickManager( - videoPlayerController: _videoController!, - autoPlay: false, - ); - _videoWidget = FlickVideoPlayer( - flickManager: _flickManager!, + _videoController = VideoPlayerController.networkUrl( + Uri.parse(_getPlayableUrl(widget.videoUrl)), ); + _videoController!.initialize().then((_) { + _flickManager = FlickManager( + videoPlayerController: _videoController!, + autoPlay: false, + ); + _videoWidget = FlickVideoPlayer( + flickManager: _flickManager!, + ); + setState(() { + _isLoading = false; + }); + }).catchError((error) { + setState(() { + _error = "Error loading video: $error"; + _isLoading = false; + }); + }); } } @@ -76,6 +104,11 @@ class VideoPlayerWidgetState extends State { @override Widget build(BuildContext context) { + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (_error != null) { + return Center(child: Text(_error!)); + } return ClipRRect( borderRadius: BorderRadius.circular(16), child: AspectRatio( diff --git a/lib/features/learning/modules/result/screens/result_screen.dart b/lib/features/learning/modules/result/screens/result_screen.dart index d7a7200..6d8bebc 100644 --- a/lib/features/learning/modules/result/screens/result_screen.dart +++ b/lib/features/learning/modules/result/screens/result_screen.dart @@ -8,7 +8,8 @@ class ResultScreen extends StatelessWidget { final String currentLevel; final String nextLevel; final int score; - final bool? isCompleted; + final bool isCompleted; + final String? stdLearningId; const ResultScreen({ super.key, @@ -16,6 +17,7 @@ class ResultScreen extends StatelessWidget { required this.nextLevel, required this.score, required this.isCompleted, + this.stdLearningId, }); @override @@ -30,20 +32,23 @@ class ResultScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (isCompleted!) + if (isCompleted) CompleteResultWidget( currentLevel: currentLevel, score: score, + stdLearningId: stdLearningId ?? '', ) else if (nextLevel != currentLevel) JumpResultWidget( nextLevel: nextLevel, score: score, + stdLearningId: stdLearningId ?? '', ) else DownResultWidget( nextLevel: nextLevel, score: score, + stdLearningId: stdLearningId ?? '', ), ], ), diff --git a/lib/features/learning/modules/result/widgets/complete_result_widget.dart b/lib/features/learning/modules/result/widgets/complete_result_widget.dart index 7946799..f6732fb 100644 --- a/lib/features/learning/modules/result/widgets/complete_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/complete_result_widget.dart @@ -1,16 +1,19 @@ import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CompleteResultWidget extends StatelessWidget { final String? currentLevel; final int? score; + final String stdLearningId; const CompleteResultWidget({ super.key, required this.currentLevel, required this.score, + required this.stdLearningId, }); @override @@ -49,12 +52,14 @@ class CompleteResultWidget extends StatelessWidget { GlobalButton( text: 'Discover More', onPressed: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => const FeedbackScreen(), - // ), - // ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => FeedbackScreen( + stdLearningId: stdLearningId, + ), + ), + ); }, ) ], diff --git a/lib/features/learning/modules/result/widgets/down_result_widget.dart b/lib/features/learning/modules/result/widgets/down_result_widget.dart index 07bfe36..00f8339 100644 --- a/lib/features/learning/modules/result/widgets/down_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/down_result_widget.dart @@ -1,15 +1,19 @@ import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/core/widgets/global_button.dart'; +import 'package:english_learning/features/learning/modules/feedback/screens/feedback_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class DownResultWidget extends StatelessWidget { final String? nextLevel; final int? score; + final String stdLearningId; + const DownResultWidget({ super.key, required this.nextLevel, required this.score, + required this.stdLearningId, }); @override @@ -58,12 +62,14 @@ class DownResultWidget extends StatelessWidget { GlobalButton( text: 'Continue', onPressed: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => const FeedbackScreen(), - // ), - // ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => FeedbackScreen( + stdLearningId: stdLearningId, + ), + ), + ); }, ) ], diff --git a/lib/features/learning/modules/result/widgets/jump_result_widget.dart b/lib/features/learning/modules/result/widgets/jump_result_widget.dart index c5ae8da..89cb251 100644 --- a/lib/features/learning/modules/result/widgets/jump_result_widget.dart +++ b/lib/features/learning/modules/result/widgets/jump_result_widget.dart @@ -7,12 +7,13 @@ import 'package:flutter_svg/svg.dart'; class JumpResultWidget extends StatelessWidget { final String? nextLevel; final int? score; + final String stdLearningId; - const JumpResultWidget({ - super.key, - required this.nextLevel, - required this.score, - }); + const JumpResultWidget( + {super.key, + required this.nextLevel, + required this.score, + required this.stdLearningId}); @override Widget build(BuildContext context) { @@ -60,10 +61,12 @@ class JumpResultWidget extends StatelessWidget { GlobalButton( text: 'Continue', onPressed: () { - Navigator.push( + Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => const FeedbackScreen(), + builder: (context) => FeedbackScreen( + stdLearningId: stdLearningId, + ), ), ); }, diff --git a/lib/features/learning/modules/topics/models/topic_model.dart b/lib/features/learning/modules/topics/models/topic_model.dart index 0be62d8..cdbaf32 100644 --- a/lib/features/learning/modules/topics/models/topic_model.dart +++ b/lib/features/learning/modules/topics/models/topic_model.dart @@ -1,22 +1,22 @@ class Topic { final String id; - final String sectionId; final String name; final String description; + final bool isCompleted; Topic({ required this.id, - required this.sectionId, required this.name, required this.description, + required this.isCompleted, }); factory Topic.fromJson(Map json) { return Topic( - id: json['ID_TOPIC'], - sectionId: json['ID_SECTION'], - name: json['NAME_TOPIC'], - description: json['DESCRIPTION_TOPIC'], + id: json['ID_TOPIC'] ?? '', + name: json['NAME_TOPIC'] ?? '', + description: json['DESCRIPTION_TOPIC'] ?? '', + isCompleted: json['IS_COMPLETED'] == 1, ); } } diff --git a/lib/features/learning/modules/topics/screens/topics_list_screen.dart b/lib/features/learning/modules/topics/screens/topics_list_screen.dart index 3da23b9..7fbe182 100644 --- a/lib/features/learning/modules/topics/screens/topics_list_screen.dart +++ b/lib/features/learning/modules/topics/screens/topics_list_screen.dart @@ -23,7 +23,9 @@ class _TopicsListScreenState extends State { @override void initState() { super.initState(); - _fetchTopics(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchTopics(); + }); } String _getFullImageUrl(String thumbnail) { @@ -39,10 +41,14 @@ class _TopicsListScreenState extends State { final token = await userProvider.getValidToken(); if (token != null) { - await Provider.of(context, listen: false) - .fetchTopics(widget.sectionId, token); + try { + await Provider.of(context, listen: false) + .fetchTopics(widget.sectionId, token); + print('Topics fetched successfully'); + } catch (e) { + print('Error fetching topics: $e'); + } } else { - // Handle the case when token is null (user might not be logged in) print('No valid token found. User might need to log in.'); } } @@ -100,7 +106,7 @@ class _TopicsListScreenState extends State { width: 90, height: 104, color: Colors.grey[300], - child: Center(child: CircularProgressIndicator()), + child: const Center(child: CircularProgressIndicator()), ); }, ), @@ -142,8 +148,7 @@ class _TopicsListScreenState extends State { return TopicCard( title: topic.name, description: topic.description, - isCompleted: - false, // You might want to implement completion tracking + isCompleted: topic.isCompleted, onTap: () { Navigator.push( context, diff --git a/lib/features/learning/modules/topics/widgets/topic_card.dart b/lib/features/learning/modules/topics/widgets/topic_card.dart index a77b06b..afd6bfd 100644 --- a/lib/features/learning/modules/topics/widgets/topic_card.dart +++ b/lib/features/learning/modules/topics/widgets/topic_card.dart @@ -63,7 +63,8 @@ class TopicCard extends StatelessWidget { isCompleted ? Icons.check_circle : Icons.radio_button_unchecked, - color: AppColors.blueColor, + color: + isCompleted ? AppColors.blueColor : AppColors.greyColor, ), ], ), diff --git a/lib/features/learning/provider/section_provider.dart b/lib/features/learning/provider/section_provider.dart index 8128873..722aaa2 100644 --- a/lib/features/learning/provider/section_provider.dart +++ b/lib/features/learning/provider/section_provider.dart @@ -12,19 +12,15 @@ class SectionProvider extends ChangeNotifier { bool get isLoading => _isLoading; String? get error => _error; - Future fetchSections(String token) async { - _isLoading = true; - _error = null; - notifyListeners(); - + Future> fetchSections(String token) async { try { _sections = await _repository.getSections(token); - _isLoading = false; notifyListeners(); + return _sections; } catch (e) { - _isLoading = false; _error = e.toString(); notifyListeners(); + return []; } } } diff --git a/lib/features/learning/widgets/section_card.dart b/lib/features/learning/widgets/section_card.dart index 0128e59..dd8c6dc 100644 --- a/lib/features/learning/widgets/section_card.dart +++ b/lib/features/learning/widgets/section_card.dart @@ -50,7 +50,7 @@ class LearningCard extends StatelessWidget { width: 90, height: 104, color: Colors.grey[300], - child: Icon( + child: const Icon( Icons.image_not_supported, color: Colors.grey, ), diff --git a/lib/my_app.dart b/lib/my_app.dart index af44e79..9c9825d 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -1,10 +1,12 @@ import 'package:english_learning/core/services/dio_client.dart'; +import 'package:english_learning/core/services/repositories/completed_topics_repository.dart'; import 'package:english_learning/core/services/repositories/exercise_repository.dart'; import 'package:english_learning/core/services/repositories/history_repository.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; import 'package:english_learning/features/auth/provider/user_provider.dart'; import 'package:english_learning/features/auth/provider/validator_provider.dart'; import 'package:english_learning/features/history/provider/history_provider.dart'; +import 'package:english_learning/features/home/provider/completed_topics_provider.dart'; import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart'; import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart'; import 'package:english_learning/features/learning/modules/topics/providers/topic_provider.dart'; @@ -18,7 +20,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -38,6 +40,9 @@ class MyApp extends StatelessWidget { ProxyProvider( update: (_, dioClient, __) => HistoryRepository(dioClient), ), + ProxyProvider( + update: (_, dioClient, __) => CompletedTopicsRepository(dioClient), + ), ChangeNotifierProxyProvider2( create: (context) => HistoryProvider( @@ -62,6 +67,14 @@ class MyApp extends StatelessWidget { userProvider, )..updateFrom(previous), ), + ChangeNotifierProxyProvider( + create: (context) => CompletedTopicsProvider( + context.read(), + ), + update: (context, completedTopicsRepository, previous) => + CompletedTopicsProvider(completedTopicsRepository), + ), ], child: Consumer( builder: (context, userProvider, _) { diff --git a/pubspec.lock b/pubspec.lock index eded28f..abc9aa0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -287,7 +287,7 @@ packages: source: sdk version: "0.0.0" flutter_cache_manager: - dependency: "direct main" + dependency: transitive description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" @@ -358,14 +358,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - flutter_logger_plus: - dependency: "direct main" - description: - name: flutter_logger_plus - sha256: c01751b8074384d116a4c1e4b549233053ea6b78fcc07b5503248227b06b923c - url: "https://pub.dev" - source: hosted - version: "5.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -600,14 +592,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - logger: - dependency: "direct main" - description: - name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" - url: "https://pub.dev" - source: hosted - version: "2.4.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9646909..1b14fd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,8 +37,6 @@ dependencies: flick_video_player: ^0.9.0 flutter: sdk: flutter - flutter_cache_manager: ^3.4.1 - flutter_logger_plus: ^5.0.0 flutter_secure_storage: ^9.2.2 flutter_svg: ^2.0.10+1 google_fonts: ^6.2.1 @@ -46,7 +44,6 @@ dependencies: image_picker: ^1.1.2 intl: ^0.19.0 jwt_decoder: ^2.0.1 - logger: ^2.4.0 provider: ^6.1.2 shared_preferences: ^2.3.2 shimmer: ^3.0.0