Fix Flutter exam parsing, make exams tab dynamic from API, and add intelligent exam resolution to submitExam
This commit is contained in:
@@ -36,21 +36,33 @@ class ExamModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ExamModel(
|
return ExamModel(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: _toInt(json['id']),
|
||||||
uuid: json['uuid']?.toString() ?? '',
|
uuid: json['uuid']?.toString() ?? '',
|
||||||
courseId: (json['course_id'] as num?)?.toInt() ?? 0,
|
courseId: _toInt(json['course_id']),
|
||||||
lessonId: (json['lesson_id'] as num?)?.toInt(),
|
lessonId: json['lesson_id'] != null ? _toInt(json['lesson_id']) : null,
|
||||||
title: json['title']?.toString() ?? 'امتحان تشخيصي',
|
title: json['title']?.toString() ?? 'امتحان تشخيصي',
|
||||||
description: json['description']?.toString() ?? '',
|
description: json['description']?.toString() ?? '',
|
||||||
scope: json['scope']?.toString() ?? 'general',
|
scope: json['scope']?.toString() ?? 'unit_exam',
|
||||||
passingPercentage: (json['passing_percentage'] as num?)?.toDouble() ?? 60.0,
|
passingPercentage: _toDouble(json['passing_percentage'], 60.0),
|
||||||
durationMinutes: (json['duration_minutes'] as num?)?.toInt() ?? 20,
|
durationMinutes: _toInt(json['duration_minutes'], 20),
|
||||||
questionsCount: (json['questions_count'] as num?)?.toInt() ?? parsedQuestions.length,
|
questionsCount: _toInt(json['questions_count'], parsedQuestions.length),
|
||||||
questions: parsedQuestions,
|
questions: parsedQuestions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _toInt(dynamic value, [int defaultValue = 0]) {
|
||||||
|
if (value == null) return defaultValue;
|
||||||
|
if (value is num) return value.toInt();
|
||||||
|
return int.tryParse(value.toString()) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _toDouble(dynamic value, [double defaultValue = 0.0]) {
|
||||||
|
if (value == null) return defaultValue;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
return double.tryParse(value.toString()) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
class QuestionModel {
|
class QuestionModel {
|
||||||
final int id;
|
final int id;
|
||||||
final String questionText;
|
final String questionText;
|
||||||
@@ -82,10 +94,10 @@ class QuestionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return QuestionModel(
|
return QuestionModel(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: _toInt(json['id']),
|
||||||
questionText: json['question_text']?.toString() ?? '',
|
questionText: json['question_text']?.toString() ?? '',
|
||||||
questionType: json['question_type']?.toString() ?? 'multiple_choice',
|
questionType: json['question_type']?.toString() ?? 'multiple_choice',
|
||||||
points: (json['points'] as num?)?.toInt() ?? 10,
|
points: _toInt(json['points'], 10),
|
||||||
topicTag: json['topic_tag']?.toString() ?? 'المفاهيم العامة',
|
topicTag: json['topic_tag']?.toString() ?? 'المفاهيم العامة',
|
||||||
explanationText: json['explanation_text']?.toString(),
|
explanationText: json['explanation_text']?.toString(),
|
||||||
aiHint: json['ai_hint']?.toString(),
|
aiHint: json['ai_hint']?.toString(),
|
||||||
@@ -109,10 +121,10 @@ class QuestionOptionModel {
|
|||||||
|
|
||||||
factory QuestionOptionModel.fromJson(Map<String, dynamic> json) {
|
factory QuestionOptionModel.fromJson(Map<String, dynamic> json) {
|
||||||
return QuestionOptionModel(
|
return QuestionOptionModel(
|
||||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
id: _toInt(json['id']),
|
||||||
optionText: json['option_text']?.toString() ?? '',
|
optionText: json['option_text']?.toString() ?? '',
|
||||||
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true,
|
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true || json['is_correct'] == '1',
|
||||||
sequenceOrder: (json['sequence_order'] as num?)?.toInt() ?? 1,
|
sequenceOrder: _toInt(json['sequence_order'], 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,15 +170,15 @@ class ExamSubmissionResultModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ExamSubmissionResultModel(
|
return ExamSubmissionResultModel(
|
||||||
attemptId: (json['attempt_id'] as num?)?.toInt() ?? 0,
|
attemptId: _toInt(json['attempt_id']),
|
||||||
score: (json['score'] as num?)?.toInt() ?? 0,
|
score: _toInt(json['score']),
|
||||||
totalScore: (json['total_score'] as num?)?.toInt() ?? 0,
|
totalScore: _toInt(json['total_score'], 100),
|
||||||
percentage: (json['percentage'] as num?)?.toDouble() ?? 0.0,
|
percentage: _toDouble(json['percentage']),
|
||||||
passed: json['passed'] == true || json['passed'] == 1,
|
passed: json['passed'] == true || json['passed'] == 1 || json['passed'] == '1',
|
||||||
rewindSeconds: (json['rewind_seconds'] as num?)?.toInt() ?? 0,
|
rewindSeconds: _toInt(json['rewind_seconds']),
|
||||||
aiDiagnosticReport: json['ai_diagnostic_report']?.toString() ?? '',
|
aiDiagnosticReport: json['ai_diagnostic_report']?.toString() ?? '',
|
||||||
weakTopics: parsedTopics,
|
weakTopics: parsedTopics,
|
||||||
tawjihiReadinessScore: (json['tawjihi_readiness_score'] as num?)?.toDouble() ?? 0.0,
|
tawjihiReadinessScore: _toDouble(json['tawjihi_readiness_score'], 50.0),
|
||||||
detailedAnswers: parsedDetails,
|
detailedAnswers: parsedDetails,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -191,10 +203,10 @@ class DetailedAnswerModel {
|
|||||||
|
|
||||||
factory DetailedAnswerModel.fromJson(Map<String, dynamic> json) {
|
factory DetailedAnswerModel.fromJson(Map<String, dynamic> json) {
|
||||||
return DetailedAnswerModel(
|
return DetailedAnswerModel(
|
||||||
questionId: (json['question_id'] as num?)?.toInt() ?? 0,
|
questionId: _toInt(json['question_id']),
|
||||||
selectedOptionId: (json['selected_option_id'] as num?)?.toInt() ?? 0,
|
selectedOptionId: _toInt(json['selected_option_id']),
|
||||||
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true,
|
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true || json['is_correct'] == '1',
|
||||||
pointsAwarded: (json['points_awarded'] as num?)?.toInt() ?? 0,
|
pointsAwarded: _toInt(json['points_awarded']),
|
||||||
explanation: json['explanation']?.toString(),
|
explanation: json['explanation']?.toString(),
|
||||||
aiHint: json['ai_hint']?.toString(),
|
aiHint: json['ai_hint']?.toString(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import '../../widgets/luxury_widgets.dart';
|
|||||||
import '../player/socratic_video_player_screen.dart';
|
import '../player/socratic_video_player_screen.dart';
|
||||||
import '../exams/adaptive_exam_screen.dart';
|
import '../exams/adaptive_exam_screen.dart';
|
||||||
import 'curriculum_document_viewer_screen.dart';
|
import 'curriculum_document_viewer_screen.dart';
|
||||||
|
import '../../../data/models/exam_model.dart';
|
||||||
|
import '../../../data/repositories/app_repositories.dart';
|
||||||
|
|
||||||
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
|
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
|
||||||
class SubjectHubScreen extends StatefulWidget {
|
class SubjectHubScreen extends StatefulWidget {
|
||||||
@@ -34,11 +36,13 @@ class SubjectHubScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerProviderStateMixin {
|
class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
|
late Future<List<ExamModel>> _examsFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tabController = TabController(length: 4, vsync: this);
|
_tabController = TabController(length: 4, vsync: this);
|
||||||
|
_examsFuture = ExamRepository().getExams(courseId: 1, scope: 'unit_exam');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -272,88 +276,128 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
|||||||
|
|
||||||
/// Tab 3: Question Bank & Adaptive Unit Exams
|
/// Tab 3: Question Bank & Adaptive Unit Exams
|
||||||
Widget _buildExamsTab(BuildContext context) {
|
Widget _buildExamsTab(BuildContext context) {
|
||||||
final exams = [
|
return FutureBuilder<List<ExamModel>>(
|
||||||
const SubjectExamModel(id: 'ex1', title: 'اختبار الفهم الشامل: الوحدة الأولى (أنظمة المعادلات)', questionsCount: 15, durationMinutes: 45, targetScore: 100),
|
future: _examsFuture,
|
||||||
const SubjectExamModel(id: 'ex2', title: 'نماذج أسئلة الوزارة للسنوات السابقة', questionsCount: 15, durationMinutes: 45, targetScore: 100),
|
builder: (context, snapshot) {
|
||||||
const SubjectExamModel(id: 'ex3', title: 'اختبار تشخيص الثغرات التكيفي بالذكاء الاصطناعي', questionsCount: 15, durationMinutes: 30, targetScore: 100),
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
];
|
return const Center(
|
||||||
|
child: CupertinoActivityIndicator(color: AppColors.saqelCyan),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
final liveExams = snapshot.data ?? [];
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
if (liveExams.isEmpty) {
|
||||||
itemCount: exams.length,
|
// Default to Primary Unit Exam if none fetched yet
|
||||||
itemBuilder: (context, idx) {
|
return ListView(
|
||||||
final exam = exams[idx];
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||||
return Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
_buildExamCard(
|
||||||
child: LuxuryCard(
|
context,
|
||||||
child: Column(
|
examId: 1,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title: 'اختبار الفهم الشامل: الوحدة الأولى (أنظمة المعادلات)',
|
||||||
children: [
|
questionsCount: 50,
|
||||||
Row(
|
durationMinutes: 45,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
children: [
|
],
|
||||||
Text(
|
);
|
||||||
exam.title,
|
}
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
|
||||||
),
|
return ListView.builder(
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
itemCount: liveExams.length,
|
||||||
decoration: BoxDecoration(
|
itemBuilder: (context, idx) {
|
||||||
color: AppColors.emeraldGreen.withAlpha(25),
|
final exam = liveExams[idx];
|
||||||
borderRadius: BorderRadius.circular(8),
|
return Padding(
|
||||||
),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: const Text(
|
child: _buildExamCard(
|
||||||
'جاهز للتقديم ✍️',
|
context,
|
||||||
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
|
examId: exam.id > 0 ? exam.id : 1,
|
||||||
),
|
title: exam.title,
|
||||||
),
|
questionsCount: exam.questionsCount > 0 ? exam.questionsCount : 50,
|
||||||
],
|
durationMinutes: exam.durationMinutes > 0 ? exam.durationMinutes : 45,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
);
|
||||||
Row(
|
},
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text('${exam.questionsCount} أسئلة', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text('${exam.durationMinutes} د', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColors.appleBlue,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
CupertinoPageRoute(
|
|
||||||
builder: (context) => AdaptiveExamScreen(
|
|
||||||
examId: idx + 1,
|
|
||||||
title: exam.title,
|
|
||||||
subjectTitle: widget.subject.title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('بدء الاختبار 🚀', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildExamCard(
|
||||||
|
BuildContext context, {
|
||||||
|
required int examId,
|
||||||
|
required String title,
|
||||||
|
required int questionsCount,
|
||||||
|
required int durationMinutes,
|
||||||
|
}) {
|
||||||
|
return LuxuryCard(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.emeraldGreen.withAlpha(25),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'جاهز للتقديم ✍️',
|
||||||
|
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('$questionsCount سؤالاً في البنك', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('$durationMinutes د', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.appleBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
CupertinoPageRoute(
|
||||||
|
builder: (context) => AdaptiveExamScreen(
|
||||||
|
examId: examId,
|
||||||
|
title: title,
|
||||||
|
subjectTitle: widget.subject.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('بدء الاختبار 🚀', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Tab 4: Official Ministry Textbooks
|
/// Tab 4: Official Ministry Textbooks
|
||||||
Widget _buildTextbooksTab(BuildContext context) {
|
Widget _buildTextbooksTab(BuildContext context) {
|
||||||
final textbooks = widget.subject.textbooks.isNotEmpty
|
final textbooks = widget.subject.textbooks.isNotEmpty
|
||||||
|
|||||||
@@ -88,7 +88,21 @@ class ExamController
|
|||||||
$isTeacher = ($request->role === 'teacher' || $request->role === 'super_admin');
|
$isTeacher = ($request->role === 'teacher' || $request->role === 'super_admin');
|
||||||
|
|
||||||
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||||
if (!$exam || $examId === 1 || (str_contains($exam['title'] ?? '', 'الوحدة الأولى') && count(Database::select("SELECT id FROM questions WHERE exam_id = ?", [$examId])) < 15)) {
|
if (!$exam) {
|
||||||
|
// Fallback: Find published unit exam with questions
|
||||||
|
$exam = Database::selectOne(
|
||||||
|
"SELECT e.* FROM exams e
|
||||||
|
JOIN questions q ON q.exam_id = e.id
|
||||||
|
WHERE e.is_published = 1 AND (e.scope = 'unit_exam' OR e.scope = 'unit_comprehensive')
|
||||||
|
GROUP BY e.id HAVING COUNT(q.id) >= 10
|
||||||
|
ORDER BY e.id DESC LIMIT 1"
|
||||||
|
);
|
||||||
|
if ($exam) {
|
||||||
|
$examId = (int)$exam['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$exam || (count(Database::select("SELECT id FROM questions WHERE exam_id = ?", [$examId])) < 15)) {
|
||||||
$examId = self::seedUnit1ComprehensiveExam();
|
$examId = self::seedUnit1ComprehensiveExam();
|
||||||
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||||
}
|
}
|
||||||
@@ -194,6 +208,42 @@ class ExamController
|
|||||||
$timeSpent = (int)($body['time_spent_seconds'] ?? 0);
|
$timeSpent = (int)($body['time_spent_seconds'] ?? 0);
|
||||||
|
|
||||||
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||||
|
if (!$exam) {
|
||||||
|
// 1. Resolve exam from submitted question IDs
|
||||||
|
if (!empty($answers) && is_array($answers)) {
|
||||||
|
$firstQId = (int)($answers[0]['question_id'] ?? 0);
|
||||||
|
if ($firstQId > 0) {
|
||||||
|
$qRow = Database::selectOne("SELECT exam_id FROM questions WHERE id = ? LIMIT 1", [$firstQId]);
|
||||||
|
if ($qRow && !empty($qRow['exam_id'])) {
|
||||||
|
$examId = (int)$qRow['exam_id'];
|
||||||
|
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resolve to latest published unit exam with questions
|
||||||
|
if (!$exam) {
|
||||||
|
$exam = Database::selectOne(
|
||||||
|
"SELECT e.* FROM exams e
|
||||||
|
JOIN questions q ON q.exam_id = e.id
|
||||||
|
WHERE e.is_published = 1 AND (e.scope = 'unit_exam' OR e.scope = 'unit_comprehensive')
|
||||||
|
GROUP BY e.id HAVING COUNT(q.id) >= 10
|
||||||
|
ORDER BY e.id DESC LIMIT 1"
|
||||||
|
);
|
||||||
|
if ($exam) {
|
||||||
|
$examId = (int)$exam['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to any published exam
|
||||||
|
if (!$exam) {
|
||||||
|
$exam = Database::selectOne("SELECT * FROM exams WHERE is_published = 1 ORDER BY id DESC LIMIT 1");
|
||||||
|
if ($exam) {
|
||||||
|
$examId = (int)$exam['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$exam) {
|
if (!$exam) {
|
||||||
$response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']);
|
$response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user