fix: listing request model and logic

This commit is contained in:
akhdanre 2025-05-04 01:15:56 +07:00
parent 9df43d451e
commit 6bf48df48a
16 changed files with 284 additions and 121 deletions

View File

@ -1,21 +1,27 @@
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
class QuizData { class QuizData {
final String authorId; final String authorId;
final String subjectId;
final String subjectName;
final String title; final String title;
final String? description; final String? description;
final bool isPublic; final bool isPublic;
final String? date; final String? date;
final String? time;
final int totalQuiz; final int totalQuiz;
final int limitDuration; final int limitDuration;
final List<QuestionListing> questionListings; final List<BaseQuestionModel> questionListings;
QuizData({ QuizData({
required this.authorId, required this.authorId,
required this.subjectId,
required this.subjectName,
required this.title, required this.title,
this.description, this.description,
required this.isPublic, required this.isPublic,
this.date, this.date,
this.time,
required this.totalQuiz, required this.totalQuiz,
required this.limitDuration, required this.limitDuration,
required this.questionListings, required this.questionListings,
@ -24,23 +30,29 @@ class QuizData {
factory QuizData.fromJson(Map<String, dynamic> json) { factory QuizData.fromJson(Map<String, dynamic> json) {
return QuizData( return QuizData(
authorId: json['author_id'], authorId: json['author_id'],
subjectId: json['subject_id'],
subjectName: json['subject_alias'],
title: json['title'], title: json['title'],
description: json['description'], description: json['description'],
isPublic: json['is_public'], isPublic: json['is_public'],
date: json['date'], date: json['date'],
time: json['time'],
totalQuiz: json['total_quiz'], totalQuiz: json['total_quiz'],
limitDuration: json['limit_duration'], limitDuration: json['limit_duration'],
questionListings: (json['question_listings'] as List).map((e) => QuestionListing.fromJson(e)).toList(), questionListings: (json['question_listings'] as List).map((e) => BaseQuestionModel.fromJson(e as Map<String, dynamic>)).toList(),
); );
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'author_id': authorId, 'author_id': authorId,
'subject_id': subjectId,
'subject_alias': subjectName,
'title': title, 'title': title,
'description': description, 'description': description,
'is_public': isPublic, 'is_public': isPublic,
'date': date, 'date': date,
'time': time,
'total_quiz': totalQuiz, 'total_quiz': totalQuiz,
'limit_duration': limitDuration, 'limit_duration': limitDuration,
'question_listings': questionListings.map((e) => e.toJson()).toList(), 'question_listings': questionListings.map((e) => e.toJson()).toList(),

View File

@ -0,0 +1,32 @@
import 'package:quiz_app/data/models/quiz/question/fill_in_the_blank_question_model.dart';
import 'package:quiz_app/data/models/quiz/question/option_question_model.dart';
import 'package:quiz_app/data/models/quiz/question/true_false_question_model.dart';
abstract class BaseQuestionModel {
final int index;
final String question;
final int duration;
final String type;
BaseQuestionModel({
required this.index,
required this.question,
required this.duration,
required this.type,
});
factory BaseQuestionModel.fromJson(Map<String, dynamic> json) {
switch (json['type']) {
case 'fill_the_blank':
return FillInTheBlankQuestion.fromJson(json);
case 'true_false':
return TrueFalseQuestion.fromJson(json);
case 'option':
return OptionQuestion.fromJson(json);
default:
throw Exception('Unsupported question type: ${json['type']}');
}
}
Map<String, dynamic> toJson();
}

View File

@ -0,0 +1,31 @@
import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
class FillInTheBlankQuestion extends BaseQuestionModel {
final String targetAnswer;
FillInTheBlankQuestion({
required int index,
required String question,
required int duration,
required this.targetAnswer,
}) : super(index: index, question: question, duration: duration, type: 'fill_the_blank');
factory FillInTheBlankQuestion.fromJson(Map<String, dynamic> json) {
return FillInTheBlankQuestion(
index: json['index'],
question: json['question'],
duration: json['duration'],
targetAnswer: json['target_answer'],
);
}
@override
Map<String, dynamic> toJson() => {
'index': index,
'question': question,
'duration': duration,
'type': type,
'target_answer': targetAnswer,
'options': null,
};
}

View File

@ -0,0 +1,34 @@
import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
class OptionQuestion extends BaseQuestionModel {
final int targetAnswer;
final List<String> options;
OptionQuestion({
required int index,
required String question,
required int duration,
required this.targetAnswer,
required this.options,
}) : super(index: index, question: question, duration: duration, type: 'option');
factory OptionQuestion.fromJson(Map<String, dynamic> json) {
return OptionQuestion(
index: json['index'],
question: json['question'],
duration: json['duration'],
targetAnswer: json['target_answer'],
options: List<String>.from(json['options']),
);
}
@override
Map<String, dynamic> toJson() => {
'index': index,
'question': question,
'duration': duration,
'type': type,
'target_answer': targetAnswer,
'options': options,
};
}

View File

@ -0,0 +1,31 @@
import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
class TrueFalseQuestion extends BaseQuestionModel {
final bool targetAnswer;
TrueFalseQuestion({
required int index,
required String question,
required int duration,
required this.targetAnswer,
}) : super(index: index, question: question, duration: duration, type: 'true_false');
factory TrueFalseQuestion.fromJson(Map<String, dynamic> json) {
return TrueFalseQuestion(
index: json['index'],
question: json['question'],
duration: json['duration'],
targetAnswer: json['target_answer'],
);
}
@override
Map<String, dynamic> toJson() => {
'index': index,
'question': question,
'duration': duration,
'type': type,
'target_answer': targetAnswer,
'options': null,
};
}

View File

@ -1,7 +1,7 @@
class QuestionListing { class QuestionListing {
final int index; final int index;
final String question; final String question;
final String targetAnswer; final dynamic targetAnswer;
final int duration; final int duration;
final String type; final String type;
final List<String>? options; final List<String>? options;

View File

@ -35,19 +35,22 @@ class QuizService extends GetxService {
} }
} }
Future<List<QuizData>> userQuiz(String userId, int page) async { Future<BaseResponseModel<List<QuizListingModel>>?> userQuiz(String userId, int page) async {
try { try {
final response = await _dio.get("${APIEndpoint.userQuiz}/$userId?page=$page"); final response = await _dio.get("${APIEndpoint.userQuiz}/$userId?page=$page");
if (response.statusCode == 200) {
final parsedResponse = BaseResponseModel<List<QuizData>>.fromJson( final parsedResponse = BaseResponseModel<List<QuizListingModel>>.fromJson(
response.data, response.data,
(data) => (data as List).map((e) => QuizData.fromJson(e as Map<String, dynamic>)).toList(), (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map<String, dynamic>)).toList(),
); );
return parsedResponse;
return parsedResponse.data ?? []; } else {
logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}");
return null;
}
} catch (e) { } catch (e) {
logC.e("Error fetching user quizzes: $e"); logC.e("Error fetching user quizzes: $e");
return []; return null;
} }
} }
@ -105,8 +108,8 @@ class QuizService extends GetxService {
logC.e("Failed to fetch quiz by id. Status: ${response.statusCode}"); logC.e("Failed to fetch quiz by id. Status: ${response.statusCode}");
return null; return null;
} }
} catch (e) { } catch (e, stacktrace) {
logC.e("Error fetching quiz by id $e"); logC.e("Error fetching quiz by id $e", stackTrace: stacktrace);
return null; return null;
} }
} }

View File

@ -20,12 +20,8 @@ class DetailQuizController extends GetxController {
} }
void loadData() async { void loadData() async {
final args = Get.arguments; final quizId = Get.arguments as String;
if (args is QuizData) { getQuizData(quizId);
data = args;
} else {
getQuizData(args);
}
} }
void getQuizData(String quizId) async { void getQuizData(String quizId) async {

View File

@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/component/widget/loading_widget.dart'; import 'package:quiz_app/component/widget/loading_widget.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart'; import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart';
class DetailQuizView extends GetView<DetailQuizController> { class DetailQuizView extends GetView<DetailQuizController> {
@ -100,7 +100,7 @@ class DetailQuizView extends GetView<DetailQuizController> {
); );
} }
Widget _buildQuestionItem(QuestionListing question, int index) { Widget _buildQuestionItem(BaseQuestionModel question, int index) {
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),

View File

@ -9,6 +9,8 @@ class HistoryController extends GetxController {
HistoryController(this._historyService, this._userController); HistoryController(this._historyService, this._userController);
RxBool isLoading = true.obs;
final historyList = <QuizHistory>[].obs; final historyList = <QuizHistory>[].obs;
@override @override
@ -19,5 +21,6 @@ class HistoryController extends GetxController {
void loadDummyHistory() async { void loadDummyHistory() async {
historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? []; historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? [];
isLoading.value = false;
} }
} }

View File

@ -13,45 +13,54 @@ class HistoryView extends GetView<HistoryController> {
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Obx(() { child: Column(
final historyList = controller.historyList; crossAxisAlignment: CrossAxisAlignment.start,
children: [
return Column( const Text(
crossAxisAlignment: CrossAxisAlignment.start, "Riwayat Kuis",
children: [ style: TextStyle(
const Text( fontSize: 24,
"Riwayat Kuis", fontWeight: FontWeight.bold,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 8), ),
const Text( const SizedBox(height: 8),
"Lihat kembali hasil kuis yang telah kamu kerjakan", const Text(
style: TextStyle( "Lihat kembali hasil kuis yang telah kamu kerjakan",
fontSize: 14, style: TextStyle(
color: Colors.grey, fontSize: 14,
), color: Colors.grey,
), ),
const SizedBox(height: 20), ),
if (historyList.isEmpty) const SizedBox(height: 20),
const Expanded( Obx(() {
child: Center(child: LoadingWidget()), if (controller.isLoading.value) {
) return Expanded(
else child: Center(
Expanded( child: LoadingWidget(),
child: ListView.builder(
itemCount: historyList.length,
itemBuilder: (context, index) {
final item = historyList[index];
return _buildHistoryCard(item);
},
), ),
) );
], }
);
}), final historyList = controller.historyList;
if (historyList.isEmpty) {
return const Expanded(
child: Center(child: Text("you still doesnt have quiz history")),
);
}
return Expanded(
child: ListView.builder(
itemCount: historyList.length,
itemBuilder: (context, index) {
final item = historyList[index];
return _buildHistoryCard(item);
},
),
);
}),
],
),
), ),
), ),
); );

View File

@ -1,11 +1,12 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/data/services/quiz_service.dart';
class LibraryController extends GetxController { class LibraryController extends GetxController {
RxList<QuizData> quizs = <QuizData>[].obs; RxList<QuizListingModel> quizs = <QuizListingModel>[].obs;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxString emptyMessage = "".obs; RxString emptyMessage = "".obs;
@ -24,11 +25,11 @@ class LibraryController extends GetxController {
void loadUserQuiz() async { void loadUserQuiz() async {
try { try {
isLoading.value = true; isLoading.value = true;
List<QuizData> data = await _quizService.userQuiz(_userController.userData!.id, currentPage); BaseResponseModel<List<QuizListingModel>>? response = await _quizService.userQuiz(_userController.userData!.id, currentPage);
if (data.isEmpty) { if (response == null) {
emptyMessage.value = "Kamu belum membuat soal."; emptyMessage.value = "Kamu belum membuat soal.";
} else { } else {
quizs.addAll(data); quizs.assignAll(response.data!);
} }
} catch (e) { } catch (e) {
emptyMessage.value = "Terjadi kesalahan saat memuat data."; emptyMessage.value = "Terjadi kesalahan saat memuat data.";
@ -38,7 +39,7 @@ class LibraryController extends GetxController {
} }
void goToDetail(int index) { void goToDetail(int index) {
Get.toNamed(AppRoutes.detailQuizPage, arguments: quizs[index]); Get.toNamed(AppRoutes.detailQuizPage, arguments: quizs[index].quizId);
} }
String formatDuration(int seconds) { String formatDuration(int seconds) {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/component/widget/loading_widget.dart'; import 'package:quiz_app/component/widget/loading_widget.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart';
import 'package:quiz_app/feature/library/controller/library_controller.dart'; import 'package:quiz_app/feature/library/controller/library_controller.dart';
class LibraryView extends GetView<LibraryController> { class LibraryView extends GetView<LibraryController> {
@ -65,7 +65,7 @@ class LibraryView extends GetView<LibraryController> {
); );
} }
Widget _buildQuizCard(QuizData quiz) { Widget _buildQuizCard(QuizListingModel quiz) {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -108,7 +108,7 @@ class LibraryView extends GetView<LibraryController> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
quiz.description ?? "", quiz.description,
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Colors.grey,
fontSize: 12, fontSize: 12,
@ -122,7 +122,7 @@ class LibraryView extends GetView<LibraryController> {
const Icon(Icons.calendar_today_rounded, size: 14, color: Colors.grey), const Icon(Icons.calendar_today_rounded, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
controller.formatDate(quiz.date ?? ""), controller.formatDate(quiz.date),
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -136,7 +136,7 @@ class LibraryView extends GetView<LibraryController> {
const Icon(Icons.access_time, size: 14, color: Colors.grey), const Icon(Icons.access_time, size: 14, color: Colors.grey),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
controller.formatDuration(quiz.limitDuration), controller.formatDuration(quiz.duration),
style: const TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
], ],

View File

@ -5,7 +5,7 @@ import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/component/notification/pop_up_confirmation.dart'; import 'package:quiz_app/component/notification/pop_up_confirmation.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart';
class QuizPlayController extends GetxController { class QuizPlayController extends GetxController {
late final QuizData quizData; late final QuizData quizData;
@ -28,7 +28,7 @@ class QuizPlayController extends GetxController {
Timer? _timer; Timer? _timer;
QuestionListing get currentQuestion => quizData.questionListings[currentIndex.value]; BaseQuestionModel get currentQuestion => quizData.questionListings[currentIndex.value];
@override @override
void onInit() { void onInit() {
@ -99,14 +99,14 @@ class QuizPlayController extends GetxController {
userAnswer = selectedAnswer.value.trim(); userAnswer = selectedAnswer.value.trim();
break; break;
} }
answeredQuestions.add(AnsweredQuestion( // answeredQuestions.add(AnsweredQuestion(
index: currentIndex.value, // index: currentIndex.value,
questionIndex: question.index, // questionIndex: question.index,
selectedAnswer: userAnswer, // selectedAnswer: userAnswer,
correctAnswer: question.targetAnswer.trim(), // correctAnswer: question.,
isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(), // isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(),
duration: currentQuestion.duration - timeLeft.value, // duration: currentQuestion.duration - timeLeft.value,
)); // ));
} }
void nextQuestion() { void nextQuestion() {
@ -136,7 +136,7 @@ class QuizPlayController extends GetxController {
void _finishQuiz() async { void _finishQuiz() async {
_timer?.cancel(); _timer?.cancel();
AppDialog.showMessage(Get.context!, "Yeay semua soal selesai"); AppDialog.showMessage(Get.context!, "Yeay semua soal selesai");
await Future.delayed(Duration(seconds: 2)); await Future.delayed(Duration(seconds: 2));
Get.offAllNamed( Get.offAllNamed(
@ -156,8 +156,8 @@ class QuizPlayController extends GetxController {
class AnsweredQuestion { class AnsweredQuestion {
final int index; final int index;
final int questionIndex; final int questionIndex;
final String selectedAnswer; final dynamic selectedAnswer;
final String correctAnswer; final dynamic correctAnswer;
final bool isCorrect; final bool isCorrect;
final int duration; final int duration;
@ -170,6 +170,17 @@ class AnsweredQuestion {
required this.duration, required this.duration,
}); });
factory AnsweredQuestion.fromJson(Map<String, dynamic> json) {
return AnsweredQuestion(
index: json['index'],
questionIndex: json['question_index'],
selectedAnswer: json['selectedAnswer'],
correctAnswer: json['correctAnswer'],
isCorrect: json['isCorrect'],
duration: json['duration'],
);
}
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'index': index, 'index': index,
'question_index': questionIndex, 'question_index': questionIndex,

View File

@ -35,7 +35,7 @@ class QuizPlayView extends GetView<QuizPlayController> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildQuestionText(), _buildQuestionText(),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildAnswerSection(), // _buildAnswerSection(),
const Spacer(), const Spacer(),
_buildNextButton(), _buildNextButton(),
], ],
@ -100,44 +100,44 @@ class QuizPlayView extends GetView<QuizPlayController> {
); );
} }
Widget _buildAnswerSection() { // Widget _buildAnswerSection() {
final question = controller.currentQuestion; // final question = controller.currentQuestion;
if (question.type == 'option' && question.options != null) { // if (question.type == 'option' && question.options != null) {
return Column( // return Column(
children: List.generate(question.options!.length, (index) { // children: List.generate(question.options!.length, (index) {
final option = question.options![index]; // final option = question.options![index];
final isSelected = controller.idxOptionSelected.value == index; // final isSelected = controller.idxOptionSelected.value == index;
return Container( // return Container(
margin: const EdgeInsets.only(bottom: 12), // margin: const EdgeInsets.only(bottom: 12),
width: double.infinity, // width: double.infinity,
child: ElevatedButton( // child: ElevatedButton(
style: ElevatedButton.styleFrom( // style: ElevatedButton.styleFrom(
backgroundColor: isSelected ? AppColors.primaryBlue : Colors.white, // backgroundColor: isSelected ? AppColors.primaryBlue : Colors.white,
foregroundColor: isSelected ? Colors.white : Colors.black, // foregroundColor: isSelected ? Colors.white : Colors.black,
side: const BorderSide(color: Colors.grey), // side: const BorderSide(color: Colors.grey),
padding: const EdgeInsets.symmetric(vertical: 14), // padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), // ),
onPressed: () => controller.selectAnswerOption(index), // onPressed: () => controller.selectAnswerOption(index),
child: Text(option), // child: Text(option),
), // ),
); // );
}), // }),
); // );
} else if (question.type == 'true_false') { // } else if (question.type == 'true_false') {
return Row( // return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ // children: [
_buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF), // _buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF),
_buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF), // _buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF),
], // ],
); // );
} else { // } else {
return GlobalTextField(controller: controller.answerTextController); // return GlobalTextField(controller: controller.answerTextController);
} // }
} // }
Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) { Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) {
return Obx(() { return Obx(() {

View File

@ -26,7 +26,7 @@ class QuizResultController extends GetxController {
final args = Get.arguments as List<dynamic>; final args = Get.arguments as List<dynamic>;
question = args[0] as QuizData; question = args[0] as QuizData;
questions = question.questionListings; // questions = question.questionListings;
answers = args[1] as List<AnsweredQuestion>; answers = args[1] as List<AnsweredQuestion>;
totalQuestions.value = questions.length; totalQuestions.value = questions.length;
} }