feat: HistoryCard navigation to direct users to ReviewScreen, and refactor various code to fix the navigation

This commit is contained in:
Naresh Pratista 2024-11-28 14:05:56 +07:00
parent e6a1c1ea1b
commit 289e16655c
13 changed files with 297 additions and 152 deletions

View File

@ -1 +1 @@
const String baseUrl = 'http://54.173.167.62/';
const String baseUrl = 'https://1252-114-6-25-184.ngrok-free.app/';

View File

@ -7,7 +7,7 @@ class HistoryRepository {
HistoryRepository(this._dioClient);
Future<List<LearningHistory>> getLearningHistory(
Future<List<HistoryModel>> getLearningHistory(
String sectionId,
String token,
) async {
@ -23,9 +23,7 @@ class HistoryRepository {
if (historyData.isEmpty) {
return []; // Mengembalikan list kosong jika tidak ada data
}
return historyData
.map((json) => LearningHistory.fromJson(json))
.toList();
return historyData.map((json) => HistoryModel.fromJson(json)).toList();
} else {
throw Exception(
'Failed to load learning history: ${response.statusMessage}');

View File

@ -1,6 +1,7 @@
import 'package:intl/intl.dart';
class LearningHistory {
class HistoryModel {
final String idStudentLearning;
final int? score;
final String currentLevel;
final String? nextLevel;
@ -9,7 +10,8 @@ class LearningHistory {
final String sectionName;
final bool isPass;
LearningHistory({
HistoryModel({
required this.idStudentLearning,
required this.score,
required this.currentLevel,
required this.nextLevel,
@ -19,8 +21,9 @@ class LearningHistory {
required this.isPass,
});
factory LearningHistory.fromJson(Map<String, dynamic> json) {
return LearningHistory(
factory HistoryModel.fromJson(Map<String, dynamic> json) {
return HistoryModel(
idStudentLearning: json['ID_STUDENT_LEARNING'],
score: json['SCORE'] ?? 0,
currentLevel: json['CURRENT_LEVEL'] ?? 'Unknown',
nextLevel: json['NEXT_LEVEL'],

View File

@ -7,7 +7,7 @@ import 'package:english_learning/features/history/models/history_model.dart';
class HistoryProvider with ChangeNotifier {
final HistoryRepository _repository;
final SectionProvider _sectionProvider;
List<LearningHistory> _learningHistory = [];
List<HistoryModel> _historyModel = [];
int _selectedPageIndex = 0;
bool _isLoading = false;
String? _error;
@ -18,7 +18,7 @@ class HistoryProvider with ChangeNotifier {
this._sectionProvider,
);
List<LearningHistory> get learningHistory => _learningHistory;
List<HistoryModel> get historyModel => _historyModel;
int get selectedPageIndex => _selectedPageIndex;
bool get isLoading => _isLoading;
String? get error => _error;
@ -46,13 +46,13 @@ class HistoryProvider with ChangeNotifier {
String firstSectionId = _sectionProvider.sections.first.id;
// Fetch history for the first section
_learningHistory =
_historyModel =
await _repository.getLearningHistory(firstSectionId, token);
_error = null;
_isInitialized = true; // Mark as initialized
} catch (e) {
_error = 'Error loading data: ${e.toString()}';
_learningHistory = [];
_historyModel = [];
} finally {
_isLoading = false;
notifyListeners();
@ -78,11 +78,11 @@ class HistoryProvider with ChangeNotifier {
try {
final history = await _repository.getLearningHistory(sectionId, token);
_learningHistory = history;
_historyModel = history;
_error = null;
} catch (e) {
_error = 'Error fetching learning history: ${e.toString()}';
_learningHistory = [];
_historyModel = [];
} finally {
_isLoading = false;
notifyListeners();
@ -151,14 +151,14 @@ class HistoryProvider with ChangeNotifier {
await _sectionProvider.fetchSections(token);
if (_sectionProvider.sections.isNotEmpty) {
String firstSectionId = _sectionProvider.sections.first.id;
_learningHistory =
_historyModel =
await _repository.getLearningHistory(firstSectionId, token);
} else {
_error = 'No sections available';
}
} catch (e) {
_error = 'Error loading data: ${e.toString()}';
_learningHistory = []; // Clear the list in case of error
_historyModel = []; // Clear the list in case of error
} finally {
_isLoading = false;
notifyListeners();

View File

@ -144,7 +144,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
}
// Tampilkan empty state jika tidak ada history
if (historyProvider.learningHistory.isEmpty) {
if (historyProvider.historyModel.isEmpty) {
return _buildEmptyState(context);
}
@ -154,8 +154,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: ListView.builder(
key: ValueKey(historyProvider.learningHistory.length),
itemCount: historyProvider.learningHistory.length,
key: ValueKey(historyProvider.historyModel.length),
itemCount: historyProvider.historyModel.length,
itemBuilder: (context, index) {
return AnimatedOpacity(
opacity: 1.0,
@ -163,7 +163,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
child: Column(
children: [
ExerciseHistoryCard(
exercise: historyProvider.learningHistory[index],
exercise: historyProvider.historyModel[index],
),
const SizedBox(height: 8.0),
],

View File

@ -1,28 +1,47 @@
import 'package:english_learning/features/history/models/history_model.dart';
import 'package:english_learning/features/history/provider/history_provider.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:english_learning/features/learning/modules/exercises/screens/review_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ExerciseHistoryCard extends StatelessWidget {
final LearningHistory exercise;
class ExerciseHistoryCard extends StatefulWidget {
final HistoryModel exercise;
const ExerciseHistoryCard({
super.key,
required this.exercise,
});
@override
State<ExerciseHistoryCard> createState() => _ExerciseHistoryCardState();
}
class _ExerciseHistoryCardState extends State<ExerciseHistoryCard> {
@override
Widget build(BuildContext context) {
final historyProvider =
Provider.of<HistoryProvider>(context, listen: false);
final color = historyProvider.getColorForLevels(
exercise.currentLevel,
exercise.nextLevel,
exercise.isPass,
widget.exercise.currentLevel,
widget.exercise.nextLevel,
widget.exercise.isPass,
);
return Card(
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ReviewScreen(
idStudentLearning: widget.exercise.idStudentLearning,
topicTitle: widget.exercise.topicName,
topicId: null,
),
),
);
},
child: Card(
color: AppColors.whiteColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
@ -38,7 +57,7 @@ class ExerciseHistoryCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
exercise.topicName,
widget.exercise.topicName,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.tetriaryTextStyle.copyWith(
fontWeight: FontWeight.w500,
@ -53,13 +72,13 @@ class ExerciseHistoryCard extends StatelessWidget {
),
children: [
TextSpan(
text: '${exercise.currentLevel}',
text: '${widget.exercise.currentLevel}',
style: AppTextStyles.blackTextStyle.copyWith(),
),
TextSpan(
text: exercise.isPass
text: widget.exercise.isPass
? 'Topic Finished'
: '${exercise.nextLevel}',
: '${widget.exercise.nextLevel}',
style: AppTextStyles.blackTextStyle.copyWith(
color: color,
),
@ -69,7 +88,7 @@ class ExerciseHistoryCard extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'Submission: ${exercise.formattedDate}',
'Submission: ${widget.exercise.formattedDate}',
style: AppTextStyles.disableTextStyle.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
@ -91,7 +110,7 @@ class ExerciseHistoryCard extends StatelessWidget {
borderRadius: BorderRadius.circular(8.0),
),
child: Text(
'${exercise.score}/100',
'${widget.exercise.score}/100',
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
@ -101,6 +120,7 @@ class ExerciseHistoryCard extends StatelessWidget {
],
),
),
),
);
}
}

View File

@ -36,7 +36,7 @@ class ReviewExerciseModel {
}
class ReviewExerciseDetail {
final String idStudentExercise;
final String? idStudentExercise;
final String idAdminExercise;
final String title;
final String question;
@ -46,14 +46,14 @@ class ReviewExerciseDetail {
final String? video;
final String? audio;
final String answerStudent;
final int isCorrect;
String answerStudent;
int isCorrect;
final double resultScoreStudent;
final List<ReviewMultipleChoice>? multipleChoices;
final List<ReviewMatchingPair>? matchingPairs;
ReviewExerciseDetail({
required this.idStudentExercise,
this.idStudentExercise,
required this.idAdminExercise,
required this.title,
required this.question,

View File

@ -374,6 +374,14 @@ class ExerciseProvider extends ChangeNotifier {
// Reset current index
_currentExerciseIndex = 0;
// Ensure all exercises are displayed with empty answers for unanswered questions
for (var exercise in _reviewExercises) {
if (exercise.answerStudent.isEmpty) {
exercise.answerStudent = ''; // Set empty answer
exercise.isCorrect = 0; // Mark as incorrect
}
}
} catch (e) {
print('Error fetching review exercises: $e');
rethrow;

View File

@ -12,15 +12,15 @@ class ExerciseScreen extends StatefulWidget {
final String? levelId;
final String studentLearningId;
final bool isReview;
final String topicId; // Tambahkan ini
final String topicTitle; // Tambahkan ini
final String topicId;
final String topicTitle;
const ExerciseScreen({
super.key,
required this.levelId,
required this.studentLearningId,
this.isReview = false,
required this.topicId,
required this.isReview,
required this.topicTitle,
});
@ -36,12 +36,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
_scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<ExerciseProvider>();
if (widget.isReview) {
provider.fetchReviewExercises(widget.studentLearningId);
} else {
provider.fetchExercises(widget.levelId!);
}
provider.setStudentLearningId(widget.studentLearningId);
});
}
@ -70,9 +65,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
}
final currentExercise = provider.currentExercise;
final hasExercises = widget.isReview
? provider.reviewExercises.isNotEmpty
: provider.exercises.isNotEmpty;
final hasExercises = provider.exercises.isNotEmpty;
if (!hasExercises || currentExercise == null) {
return const Scaffold(
@ -85,15 +78,6 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
leading: widget.isReview
? IconButton(
icon: const Icon(
BootstrapIcons.arrow_left,
color: AppColors.whiteColor,
),
onPressed: () => Navigator.of(context).pop(),
)
: null, // Show back button only in review mode
iconTheme: const IconThemeData(color: AppColors.whiteColor),
centerTitle: true,
title: Text(
@ -108,8 +92,7 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
gradient: AppColors.gradientTheme,
),
),
actions: !widget.isReview
? [
actions: [
IconButton(
icon: const Icon(
BootstrapIcons.info_circle,
@ -118,13 +101,11 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
),
onPressed: () => _showInstructions(context),
),
]
: null,
],
),
body: SafeArea(
child: Column(
children: [
if (!widget.isReview)
const Padding(
padding: EdgeInsets.all(16.0),
child: ExerciseProgress(),
@ -140,12 +121,12 @@ class _ExerciseScreenState extends State<ExerciseScreen> {
ExerciseContent(
key: ValueKey(provider.currentExerciseIndex),
exercise: currentExercise,
isReview: widget.isReview,
isReview: false,
),
const SizedBox(height: 24),
ExerciseNavigator(
onScrollToTop: _scrollToTop,
isReview: widget.isReview,
isReview: false,
topicId: widget.topicId,
topicTitle: widget.topicTitle,
),

View File

@ -0,0 +1,122 @@
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:english_learning/features/learning/modules/exercises/providers/exercise_provider.dart';
import 'package:english_learning/features/learning/modules/exercises/widgets/content/exercise_content.dart';
import 'package:english_learning/features/learning/modules/exercises/widgets/exercise_navigator.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
class ReviewScreen extends StatefulWidget {
final String idStudentLearning;
final String? topicId;
final String topicTitle;
const ReviewScreen({
super.key,
required this.idStudentLearning,
this.topicId,
required this.topicTitle,
});
@override
State<ReviewScreen> createState() => _ReviewScreenState();
}
class _ReviewScreenState extends State<ReviewScreen> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<ExerciseProvider>();
provider.fetchReviewExercises(widget.idStudentLearning);
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
@override
Widget build(BuildContext context) {
return Consumer<ExerciseProvider>(builder: (context, provider, child) {
if (provider.isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final currentExercise = provider.currentExercise;
final hasExercises = provider.reviewExercises.isNotEmpty;
if (!hasExercises || currentExercise == null) {
return const Scaffold(
body: Center(child: Text('No review exercises available')),
);
}
return Scaffold(
backgroundColor: AppColors.bgSoftColor,
appBar: AppBar(
elevation: 0,
centerTitle: true,
title: Text(
'${provider.nameTopic} - ${provider.nameLevel}',
style: AppTextStyles.whiteTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.w900,
),
),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: AppColors.gradientTheme,
),
),
leading: IconButton(
icon: const Icon(
BootstrapIcons.arrow_left,
color: AppColors.whiteColor,
),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
const SizedBox(height: 16),
ExerciseContent(
key: ValueKey(provider.currentExerciseIndex),
exercise: currentExercise,
isReview: true,
),
const SizedBox(height: 24),
ExerciseNavigator(
onScrollToTop: _scrollToTop,
isReview: true,
topicId: widget.topicId ?? '',
topicTitle: widget.topicTitle,
),
const SizedBox(height: 32),
],
),
),
),
);
});
}
}

View File

@ -12,14 +12,14 @@ import 'package:provider/provider.dart';
class ExerciseNavigator extends StatelessWidget {
final VoidCallback? onScrollToTop;
final bool isReview;
final String topicId;
final String? topicId;
final String topicTitle;
const ExerciseNavigator({
super.key,
required this.onScrollToTop,
this.isReview = false,
required this.topicId,
this.topicId,
required this.topicTitle,
});
@ -48,7 +48,7 @@ class ExerciseNavigator extends StatelessWidget {
score: int.tryParse(result['SCORE'].toString()) ?? 0,
isCompleted: result['IS_PASS'] == 1,
stdLearningId: result['STUDENT_LEARNING_ID']?.toString(),
topicId: topicId,
topicId: topicId!,
topicTitle: topicTitle,
),
),

View File

@ -1,6 +1,7 @@
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/exercises/screens/review_screen.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';
@ -33,11 +34,9 @@ class PretestCard extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MaterialScreen(
levelId: pretest.idLevel,
isReview: true,
studentLearningId: pretest.idStudentLearning,
topicId: pretest.idTopic,
builder: (context) => ReviewScreen(
idStudentLearning: pretest.idStudentLearning!,
topicId: null,
topicTitle: pretest.nameTopic,
),
),

View File

@ -4,6 +4,7 @@ import 'package:english_learning/core/widgets/custom_snackbar.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/learning/modules/exercises/screens/exercise_screen.dart';
import 'package:english_learning/features/learning/modules/exercises/screens/review_screen.dart';
import 'package:english_learning/features/learning/modules/level/providers/level_provider.dart';
import 'package:english_learning/features/learning/modules/material/widgets/audio_player_widget.dart';
import 'package:english_learning/features/learning/modules/material/widgets/image_widget.dart';
@ -67,8 +68,8 @@ class _MaterialScreenState extends State<MaterialScreen>
Future<void> _createStudentLearning() async {
if (widget.isReview && widget.studentLearningId != null) {
// Jika mode review dan studentLearningId tersedia, langsung navigasi ke ExerciseScreen
_navigateToExercise(widget.studentLearningId!);
// Jika mode review dan studentLearningId tersedia, langsung navigasi ke ReviewScreen
_navigateToReview(widget.studentLearningId!);
return;
}
setState(() {
@ -102,7 +103,20 @@ class _MaterialScreenState extends State<MaterialScreen>
builder: (context) => ExerciseScreen(
levelId: widget.levelId,
studentLearningId: studentLearningId,
isReview: widget.isReview,
isReview: false,
topicId: widget.topicId,
topicTitle: widget.topicTitle,
),
),
);
}
void _navigateToReview(String studentLearningId) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ReviewScreen(
idStudentLearning: studentLearningId,
topicId: widget.topicId,
topicTitle: widget.topicTitle,
),