453 lines
15 KiB
Dart
453 lines
15 KiB
Dart
import 'package:bahasajepang/pages/pretest.model.dart';
|
|
import 'package:bahasajepang/service/API_config.dart';
|
|
import 'package:bahasajepang/theme.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'dart:convert';
|
|
import 'dart:async';
|
|
|
|
class PretestPage extends StatefulWidget {
|
|
const PretestPage({super.key});
|
|
|
|
@override
|
|
_PretestPageState createState() => _PretestPageState();
|
|
}
|
|
|
|
class _PretestPageState extends State<PretestPage> {
|
|
int currentQuestionIndex = 0;
|
|
List<int?> userAnswers = List.filled(pretestQuestions.length, null);
|
|
int? userId;
|
|
late Timer _timer;
|
|
int _remainingTime = 120; // 2 menit dalam detik
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
loadUserId();
|
|
_startTimer();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startTimer() {
|
|
const oneSec = Duration(seconds: 1);
|
|
_timer = Timer.periodic(
|
|
oneSec,
|
|
(Timer timer) {
|
|
if (_remainingTime == 0) {
|
|
timer.cancel();
|
|
_timeUp();
|
|
} else {
|
|
setState(() {
|
|
_remainingTime--;
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
void _timeUp() {
|
|
for (int i = 0; i < userAnswers.length; i++) {
|
|
if (userAnswers[i] == null) {
|
|
userAnswers[i] = -1;
|
|
}
|
|
}
|
|
final result = calculateTestResult();
|
|
_handleTestResult(result);
|
|
}
|
|
|
|
Future<void> loadUserId() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
userId = prefs.getInt('id');
|
|
});
|
|
}
|
|
|
|
void answerQuestion(int answerIndex) {
|
|
setState(() {
|
|
userAnswers[currentQuestionIndex] = answerIndex;
|
|
});
|
|
|
|
if (currentQuestionIndex < pretestQuestions.length - 1) {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
setState(() {
|
|
currentQuestionIndex++;
|
|
});
|
|
});
|
|
} else {
|
|
_timer.cancel();
|
|
final result = calculateTestResult();
|
|
_handleTestResult(result);
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> calculateTestResult() {
|
|
int pemulaCorrect = 0;
|
|
int n5Correct = 0;
|
|
|
|
for (int i = 0; i < pretestQuestions.length; i++) {
|
|
if (userAnswers[i] == pretestQuestions[i]["correctAnswer"]) {
|
|
if (pretestQuestions[i]["level"] == "pemula") {
|
|
pemulaCorrect++;
|
|
} else {
|
|
n5Correct++;
|
|
}
|
|
}
|
|
}
|
|
|
|
int totalScore =
|
|
((pemulaCorrect + n5Correct) / pretestQuestions.length * 100).round();
|
|
|
|
int recommendedLevel;
|
|
if (totalScore < 30) {
|
|
recommendedLevel = 1;
|
|
} else if (totalScore < 60) {
|
|
recommendedLevel = 2;
|
|
} else {
|
|
recommendedLevel = 3;
|
|
}
|
|
|
|
return {
|
|
"totalScore": totalScore,
|
|
"pemulaCorrect": pemulaCorrect,
|
|
"n5Correct": n5Correct,
|
|
"recommendedLevel": recommendedLevel,
|
|
"totalQuestions": pretestQuestions.length,
|
|
};
|
|
}
|
|
|
|
Future<void> _handleTestResult(Map<String, dynamic> result) async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
await prefs.setInt('level_id', result["recommendedLevel"]);
|
|
await sendLevelToDatabase(result["recommendedLevel"]);
|
|
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text("Hasil Pretest"),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Skor Anda: ${result["totalScore"]}%"),
|
|
const SizedBox(height: 8),
|
|
Text("Benar (Pemula): ${result["pemulaCorrect"]}/10"),
|
|
Text("Benar (N5): ${result["n5Correct"]}/10"),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
"Level yang direkomendasikan: ${_getLevelName(result["recommendedLevel"])}",
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_navigateToLevel(result["recommendedLevel"]);
|
|
},
|
|
child: const Text("LANJUT"),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> sendLevelToDatabase(int level_id) async {
|
|
const endpoint = "/update-level";
|
|
try {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
int? userId = prefs.getInt('id');
|
|
|
|
if (userId == null) {
|
|
print('User ID tidak ditemukan');
|
|
return;
|
|
}
|
|
|
|
var response = await http.post(
|
|
Uri.parse(ApiConfig.baseUrl + endpoint),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({'user_id': userId, 'level_id': level_id}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
var jsonResponse = jsonDecode(response.body);
|
|
print('Response: ${jsonResponse}');
|
|
} else {
|
|
print('Failed to send level. Status Code: ${response.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
print('Error: $e');
|
|
}
|
|
}
|
|
|
|
String _getLevelName(int level) {
|
|
switch (level) {
|
|
case 1:
|
|
return "Pemula";
|
|
case 2:
|
|
return "N5";
|
|
case 3:
|
|
return "N4";
|
|
default:
|
|
return "-";
|
|
}
|
|
}
|
|
|
|
void _navigateToLevel(int level) {
|
|
final routes = {
|
|
1: '/pemula',
|
|
2: '/n5',
|
|
3: '/n4',
|
|
};
|
|
|
|
if (routes.containsKey(level)) {
|
|
Navigator.pushNamedAndRemoveUntil(
|
|
context, routes[level]!, (route) => false);
|
|
}
|
|
}
|
|
|
|
String _formatTime(int seconds) {
|
|
int minutes = seconds ~/ 60;
|
|
int remainingSeconds = seconds % 60;
|
|
return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final question = pretestQuestions[currentQuestionIndex];
|
|
final bool isTimeCritical = _remainingTime <= 30;
|
|
|
|
return Scaffold(
|
|
backgroundColor: bgColor1,
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Tes Penempatan',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
backgroundColor: bgColor3,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
iconTheme: const IconThemeData(color: Colors.black),
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
bottom: Radius.circular(20),
|
|
),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Column(
|
|
children: [
|
|
SizedBox(height: 20),
|
|
// Header Section
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: bgColor2.withOpacity(0.8),
|
|
borderRadius: BorderRadius.circular(15),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: bgColor3.withOpacity(0.2),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Timer with animated background
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: isTimeCritical
|
|
? Colors.red.withOpacity(0.2)
|
|
: bgColor3.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.timer,
|
|
color: isTimeCritical ? Colors.red : bgColor3,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_formatTime(_remainingTime),
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: isTimeCritical ? Colors.red : bgColor3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Progress indicator with label
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Soal ${currentQuestionIndex + 1}/${pretestQuestions.length}",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(0.8),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
LinearProgressIndicator(
|
|
value: (currentQuestionIndex + 1) /
|
|
pretestQuestions.length,
|
|
backgroundColor: bgColor1,
|
|
color: bgColor3,
|
|
minHeight: 6,
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Question Card
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
color: Colors.white,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
question["question"],
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: bgColor3,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
"Pilih jawaban yang benar:",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Options List
|
|
ListView.separated(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
shrinkWrap: true,
|
|
itemCount: question["options"].length,
|
|
separatorBuilder: (context, index) =>
|
|
const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final option = question["options"][index];
|
|
final isSelected =
|
|
userAnswers[currentQuestionIndex] == index;
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: isSelected ? bgColor3 : Colors.white,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: () => answerQuestion(index),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isSelected
|
|
? Colors.white
|
|
: bgColor3,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: isSelected
|
|
? Icon(
|
|
Icons.check,
|
|
size: 16,
|
|
color: Colors.white,
|
|
)
|
|
: null,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Text(
|
|
option,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: isSelected
|
|
? Colors.white
|
|
: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|