diff --git a/assets/audio/Minalmukni.mp3 b/assets/audio/Minalmukni.mp3 new file mode 100644 index 0000000..2d9c9c0 Binary files /dev/null and b/assets/audio/Minalmukni.mp3 differ diff --git a/assets/audio/aiuba.mp3 b/assets/audio/aiuba.mp3 new file mode 100644 index 0000000..3f9a242 Binary files /dev/null and b/assets/audio/aiuba.mp3 differ diff --git a/assets/audio/uananiakna.mp3 b/assets/audio/uananiakna.mp3 new file mode 100644 index 0000000..a1cf513 Binary files /dev/null and b/assets/audio/uananiakna.mp3 differ diff --git a/lib/core/baseurl/base_url.dart b/lib/core/baseurl/base_url.dart index 1e47b9d..7f9787f 100644 --- a/lib/core/baseurl/base_url.dart +++ b/lib/core/baseurl/base_url.dart @@ -1,4 +1,5 @@ // lib/config.dart class BaseUrl { static const String baseUrl = 'https://legal-marginally-macaque.ngrok-free.app/api'; + static const String audioUrl = 'https://legal-marginally-macaque.ngrok-free.app'; } diff --git a/lib/core/navigation/navigation_pengajar.dart b/lib/core/navigation/navigation_pengajar.dart index 3afccc2..d44176c 100644 --- a/lib/core/navigation/navigation_pengajar.dart +++ b/lib/core/navigation/navigation_pengajar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ta_tahsin/core/theme.dart'; -import 'package:ta_tahsin/view/pengajar/data_latihan/data_latihan.dart'; +import 'package:ta_tahsin/view/pengajar/data_latihan/validasi_pelafalan.dart'; import '../../view/pengajar/data_santri/data_santri.dart'; import '../../view/pengajar/kemajuan/kemajuan.dart'; @@ -24,7 +24,7 @@ class _NavigationPengajarPageState extends State { final List _pages = [ const KemajuanPage(), const DataSantriPage(), - // const DataLatihanPage(), + const ValidasiPelafalan(), const PengajarProfilePage(), ]; @@ -52,10 +52,10 @@ class _NavigationPengajarPageState extends State { icon: Icon(Icons.list_alt), label: "Data Santri", ), - // BottomNavigationBarItem( - // icon: Icon(Icons.list_alt), - // label: "Data Latihan", - // ), + BottomNavigationBarItem( + icon: Icon(Icons.list_alt), + label: "Penilaian", + ), BottomNavigationBarItem( icon: Icon(Icons.person), label: "Profile", diff --git a/lib/core/router/route.dart b/lib/core/router/route.dart index 377c51a..4dee905 100644 --- a/lib/core/router/route.dart +++ b/lib/core/router/route.dart @@ -3,11 +3,13 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ta_tahsin/view/auth/changePass/ubah_password.dart'; import 'package:ta_tahsin/view/home/latihan/latihan.dart'; +import 'package:ta_tahsin/view/home/penilaian/penilaian.dart'; import 'package:ta_tahsin/view/home/profile/edit_profile.dart'; import 'package:ta_tahsin/view/home/progres/progres.dart'; import 'package:ta_tahsin/view/home/quiz/detail_quiz.dart'; import 'package:ta_tahsin/view/home/quiz/hasil_quiz.dart'; -import 'package:ta_tahsin/view/pengajar/data_latihan/detail_data_latihan.dart'; +import 'package:ta_tahsin/view/pengajar/data_latihan/detail_validasi_pelafalan.dart'; +import 'package:ta_tahsin/view/pengajar/data_latihan/hasil_validasi.dart'; import 'package:ta_tahsin/view/pengajar/data_santri/detail_data_santri.dart'; import 'package:ta_tahsin/view/pengajar/data_santri/tambah_santri.dart'; import 'package:ta_tahsin/view/pengajar/kemajuan/detail_kemajuan.dart'; @@ -136,15 +138,15 @@ GoRoute( return TambahSantriPage(); // Halaman Data Santri untuk pengajar }, ), - GoRoute( - path: '/detail_data_latihan', - builder: (BuildContext context, GoRouterState state) { - final Map extra = state.extra as Map; - return DetailDataLatihanPage( - id: extra['id'], - ); - }, - ), + // GoRoute( + // path: '/detail_data_latihan', + // builder: (BuildContext context, GoRouterState state) { + // final Map extra = state.extra as Map; + // return DetailDataLatihanPage( + // id: extra['id'], + // ); + // }, + // ), GoRoute( path: '/detail_quiz', builder: (BuildContext context, GoRouterState state) { @@ -200,5 +202,34 @@ GoRoute( ); }, ), + GoRoute( + path: '/detail_validasi', + builder: (BuildContext context, GoRouterState state) { + final Map extra = state.extra as Map; + return DetailValidasiPelafalan( + user_id: extra['user_id'], // nilainya null + sub_materi_id: extra['sub_materi_id'], // nilainya null +); + }, + ), + GoRoute( + path: '/hasil_validasi', + builder: (BuildContext context, GoRouterState state) { + final Map extra = state.extra as Map; + return HasilValidasiPage( + user_id: extra['user_id'], // nilainya null + sub_materi_id: extra['sub_materi_id'], // nilainya null +); + }, + ), + GoRoute( + path: '/penilaian', + builder: (BuildContext context, GoRouterState state) { + final Map extra = state.extra as Map; + return PenilaianPage( + sub_materi_id: extra['sub_materi_id'], + ); + }, + ), ], ); diff --git a/lib/view/auth/login/login.dart b/lib/view/auth/login/login.dart index b5131c0..c750841 100644 --- a/lib/view/auth/login/login.dart +++ b/lib/view/auth/login/login.dart @@ -65,6 +65,10 @@ class _LoginPageState extends State { String peran = data['data']['user']['peran']; prefs.setString('peran', peran); + // Simpan user_id ke dalam SharedPreferences + int userId = data['data']['user']['id']; + prefs.setInt('user_id', userId); // Menyimpan user_id sebagai integer + if (peran == 'santri') { router.push("/navigasi"); } else if (peran == 'pengajar') { diff --git a/lib/view/home/latihan/latihan.dart b/lib/view/home/latihan/latihan.dart index 8093438..fa16c3c 100644 --- a/lib/view/home/latihan/latihan.dart +++ b/lib/view/home/latihan/latihan.dart @@ -91,9 +91,9 @@ class _LatihanPageState extends State { Future _startRecording() async { if (await Permission.microphone.request().isGranted) { final directory = Directory.systemTemp; - final fileName = DateTime.now().millisecondsSinceEpoch.toString(); - final path = '${directory.path}/record_voice_$fileName.m4a'; - await _record.start( +final fileName = DateTime.now().millisecondsSinceEpoch.toString(); +final path = '${directory.path}/record_voice_$fileName.m4a'; // Pastikan file disimpan dengan ekstensi .m4a +await _record.start( const RecordConfig(), path: path, ); @@ -103,60 +103,109 @@ class _LatihanPageState extends State { } } - // // Fungsi untuk menghentikan perekaman - // Future stopRecording() async { - // await _record.stop(); - // setState(() { - // isRecording = false; - // }); - // } +// Future uploadRecording(String filePath, int subMateriId, int idLatihan) async { +// SharedPreferences prefs = await SharedPreferences.getInstance(); +// String? authToken = prefs.getString('token'); +// var uri = Uri.parse('${BaseUrl.baseUrl}/upload_audio'); +// var request = http.MultipartRequest('POST', uri); -// // In stopRecording(), save the recorded audio file path to the database -// Future stopRecording() async { -// await _record.stop(); -// setState(() { -// isRecording = false; -// }); - -// debugPrint('ID Latihan: ${widget.id}'); -// debugPrint('File Path: $recordedFilePath'); -// // After stopping the recording, save the audio file name/path to the backend -// if (recordedFilePath != null) { -// saveRecordedAudioName(recordedFilePath!); // Call the save function to API -// } +// final extension = filePath.split('.').last.toLowerCase(); +// if (!['m4a', 'mp3', 'wav'].contains(extension)) { +// print("File format is not supported. Please use m4a, mp3, or wav."); +// return; // } -Future uploadRecording(String filePath) async { + + +// var file = await http.MultipartFile.fromPath( +// 'recorded_audio', +// filePath, +// contentType: MediaType('audio', 'm4a'), +// ); +// request.files.add(file); + +// // Tambahkan sub_materi_id dan id_latihan +// request.fields['sub_materi_id'] = subMateriId.toString(); +// request.fields['id_latihan'] = idLatihan.toString(); + +// request.headers.addAll({ +// 'Authorization': 'Bearer $authToken', +// 'Accept': 'application/json', +// }); + +// try { +// var response = await request.send(); +// var responseBody = await response.stream.bytesToString(); + +// if (response.statusCode == 200) { +// print('Upload berhasil!'); +// print('Response body: $responseBody'); +// } else { +// print('Gagal mengupload file: ${response.statusCode}'); +// print('Response body: $responseBody'); +// } +// } catch (e) { +// print('Terjadi kesalahan saat mengupload: $e'); +// } +// } + + +Future uploadRecording(String filePath, int subMateriId, int idLatihan) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - String? authToken = prefs.getString('token'); + String? authToken = prefs.getString('token'); var uri = Uri.parse('${BaseUrl.baseUrl}/upload_audio'); var request = http.MultipartRequest('POST', uri); - // Menambahkan file rekaman ke dalam request - var file = await http.MultipartFile.fromPath( - 'recorded_audio', // Nama field yang akan diterima di Laravel - filePath, - // contentType: MediaType('file', 'm4a'), // Sesuaikan dengan jenis file - ); + final extension = filePath.split('.').last.toLowerCase(); + if (!['m4a', 'mp3', 'wav'].contains(extension)) { + print("File format is not supported. Please use m4a, mp3, or wav."); + return; + } + var file = await http.MultipartFile.fromPath( + 'recorded_audio', + filePath, + contentType: MediaType('audio', 'm4a'), + ); request.files.add(file); - // Kirimkan request - // Menambahkan header Authorization - request.headers.addAll({ - 'Authorization': 'Bearer $authToken', // Menambahkan token ke dalam header - }); - var response = await request.send(); + request.fields['sub_materi_id'] = subMateriId.toString(); + request.fields['id_latihan'] = idLatihan.toString(); - if (response.statusCode == 200) { - print('Upload berhasil!'); - } else { - print('Gagal mengupload file: ${response.statusCode}'); + request.headers.addAll({ + 'Authorization': 'Bearer $authToken', + 'Accept': 'application/json', + }); + + try { + var response = await request.send(); + var responseBody = await response.stream.bytesToString(); + + if (response.statusCode == 200) { + print('Upload berhasil!'); + print('Response body: $responseBody'); + + // Parse response body + final decoded = json.decode(responseBody); + + // Ambil id dari progress + final int progressId = decoded['progress']['id']; + + // Simpan id ke SharedPreferences + await storeProgressId(progressId); + } else { + print('Gagal mengupload file: ${response.statusCode}'); + print('Response body: $responseBody'); + } + } catch (e) { + print('Terjadi kesalahan saat mengupload: $e'); } } + + Future stopRecording() async { await _record.stop(); - await uploadRecording(recordedFilePath!); + // await uploadRecording(recordedFilePath!); // Check if the widget is still mounted before calling setState if (mounted) { @@ -170,7 +219,6 @@ Future stopRecording() async { final latihan = latihanList[widget.currentStep]; // Get the latihan at current step final idLatihan = latihan['id']; // Use the 'id' from latihan data - await storeLatihanId(idLatihan); // Debug print to show the file path and latihan ID debugPrint('ID Latihan: $idLatihan'); @@ -178,58 +226,19 @@ Future stopRecording() async { // After stopping the recording, save the audio file name/path to the backend if (recordedFilePath != null) { - saveRecordedAudioName(idLatihan, recordedFilePath!); // Pass the latihan ID to the save function + // saveRecordedAudioName(idLatihan, recordedFilePath!); // Pass the latihan ID to the save function + await uploadRecording(recordedFilePath!, widget.id, idLatihan); } } -Future saveRecordedAudioName(int idLatihan, String filePath) async { +Future storeProgressId(int idProgress) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - String? authToken = prefs.getString('token'); - debugPrint("Token yang diambil: $authToken"); - - // Check if filePath is valid - if (filePath.isEmpty) { - debugPrint("Error: File path is empty."); - return; // Exit if the file path is invalid - } - - final String apiUrl = '${BaseUrl.baseUrl}/latihan/$idLatihan/saverecord'; // API endpoint with the latihan ID - - try { - final response = await http.put( - Uri.parse(apiUrl), - headers: { - 'Authorization': 'Bearer $authToken', // Authentication token - 'Content-Type': 'application/json', - }, - body: jsonEncode({ - 'recorded_audio': filePath, // Send the file path as parameter - }), - ); - - // Check if the response status is successful - if (response.statusCode == 200) { - debugPrint('Audio file saved successfully'); - } else { - // Log more detailed error information - debugPrint('Failed to save audio file'); - debugPrint('Status Code: ${response.statusCode}'); - debugPrint('Response Body: ${response.body}'); - } - } catch (e) { - // Catch any error that occurs during the request and log it - debugPrint("Error saving audio file: $e"); - } -} - -Future storeLatihanId(int idLatihan) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - List latihanIds = prefs.getStringList('latihanIds')?.map((e) => int.parse(e)).toList() ?? []; - latihanIds.add(idLatihan); // Add the new id to the list + List progressIds = prefs.getStringList('progressIds')?.map((e) => int.parse(e)).toList() ?? []; + progressIds.add(idProgress); // Add the new id to the list // Store the list as a string (in SharedPreferences) - await prefs.setStringList('latihanIds', latihanIds.map((e) => e.toString()).toList()); - debugPrint("Stored Latihan IDs: $latihanIds"); + await prefs.setStringList('progressIds', progressIds.map((e) => e.toString()).toList()); + debugPrint("Stored progress IDs: $progressIds"); } void stopTimer() { @@ -252,6 +261,13 @@ Future storeLatihanId(int idLatihan) async { print("Audio playing..."); } + Future clearProgressIds() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove('progressIds'); + debugPrint("progressIds cleared from SharedPreferences."); +} + + @override Widget build(BuildContext context) { @@ -259,7 +275,8 @@ Future storeLatihanId(int idLatihan) async { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close), - onPressed: () { + onPressed: () async { + await clearProgressIds(); // Hapus data sebelum navigasi context.go('/navigasi'); }, ), diff --git a/lib/view/home/latihan/pelafalan_popup.dart b/lib/view/home/latihan/pelafalan_popup.dart index e1fc5b8..4200de9 100644 --- a/lib/view/home/latihan/pelafalan_popup.dart +++ b/lib/view/home/latihan/pelafalan_popup.dart @@ -97,48 +97,52 @@ class _PelafalanPageState extends State { // } // } - Future updateProgress(int submateriId) async { + Future updateStatusProgress() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - String? authToken = prefs.getString('token'); // Get the auth token from shared preferences - final String apiUrl = '${BaseUrl.baseUrl}/progress/$submateriId/save'; // Include submateri_id in URL + String? authToken = prefs.getString('token'); + final String apiUrl = '${BaseUrl.baseUrl}/update_progress_status'; - // Retrieve stored latihan ids from SharedPreferences - List latihanIds = prefs.getStringList('latihanIds')?.map((e) => int.parse(e)).toList() ?? []; + // Ambil list ID progress yang tersimpan + List progressIds = prefs.getStringList('progressIds')?.map((e) => int.parse(e)).toList() ?? []; - // If there are no latihan IDs, show an error and return - if (latihanIds.isEmpty) { - debugPrint('No latihan IDs found to update progress.'); + if (progressIds.isEmpty) { + debugPrint('No progress IDs found to update.'); return; } - final response = await http.post( - Uri.parse(apiUrl), - headers: { - 'Authorization': 'Bearer $authToken', // Send the auth token for authorization - 'Content-Type': 'application/json', - }, - body: jsonEncode({ - 'latihan_ids': latihanIds, // Pass the array of latihan IDs - }), - ); + try { + final response = await http.post( + Uri.parse(apiUrl), + headers: { + 'Authorization': 'Bearer $authToken', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'progress_ids': progressIds, + }), + ); - if (response.statusCode == 200) { - // If successful, show completion dialog - _showCompletionDialog(context); + if (response.statusCode == 200) { + debugPrint('Progress status updated successfully.'); + _showCompletionDialog(context); - // After updating, remove the stored latihan IDs from SharedPreferences - await clearLatihanIds(); - } else { - // Handle failure response - print('Failed to update progress'); + // Hapus setelah update + } else { + debugPrint('Failed to update progress status. Code: ${response.statusCode}'); + debugPrint('Body: ${response.body}'); + } + } catch (e) { + debugPrint('Error updating progress status: $e'); } } + // Function to remove latihan ids from SharedPreferences after the action Future clearLatihanIds() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.remove('latihanIds'); - debugPrint("Latihan IDs cleared from SharedPreferences."); + await prefs.remove('progressIds'); + debugPrint("progressIds cleared from SharedPreferences."); } @@ -318,7 +322,7 @@ Future clearLatihanIds() async { crossAxisAlignment: CrossAxisAlignment.center, children: [ ElevatedButton( - onPressed: () { + onPressed: () async { if (currentStep < latihanData.length - 1) { setState(() { currentStep++; // Update step saat lanjut @@ -333,7 +337,9 @@ Future clearLatihanIds() async { } else { // updateProgress(widget.id); - updateProgress(widget.id); // Pass submateri_id (widget.id) and latihan_ids + + updateStatusProgress(); // Pass submateri_id (widget.id) and latihan_ids + await clearLatihanIds(); } }, style: ElevatedButton.styleFrom( diff --git a/lib/view/home/materi/materi.dart b/lib/view/home/materi/materi.dart index b91dc85..80ca7c6 100644 --- a/lib/view/home/materi/materi.dart +++ b/lib/view/home/materi/materi.dart @@ -314,9 +314,12 @@ class _MateriPageState extends State { ), child: ElevatedButton( onPressed: () { - debugPrint( - 'tapped detail hasil penilaian', - ); + context.push( + '/penilaian', + extra: { + 'sub_materi_id': submateri['id'], + }, + ); }, style: ElevatedButton.styleFrom( foregroundColor: diff --git a/lib/view/home/penilaian/penilaian.dart b/lib/view/home/penilaian/penilaian.dart index e69de29..765a32d 100644 --- a/lib/view/home/penilaian/penilaian.dart +++ b/lib/view/home/penilaian/penilaian.dart @@ -0,0 +1,239 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ta_tahsin/core/baseurl/base_url.dart'; +import 'package:ta_tahsin/core/theme.dart'; + +class PenilaianPage extends StatefulWidget { + // final int user_id; + final int sub_materi_id; + + const PenilaianPage({ + super.key, + // required this.user_id, + required this.sub_materi_id, + }); + + @override + _PenilaianPageState createState() => _PenilaianPageState(); +} + +class _PenilaianPageState extends State { + bool isLoading = true; + List progressData = []; + int totalNilai = 0; + String status = ""; + + @override + void initState() { + super.initState(); + fetchProgressData(); + } + + Future fetchProgressData() async { + // Ambil SharedPreferences untuk mendapatkan token dan user_id + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); // Mengambil token dari SharedPreferences + int userId = prefs.getInt('user_id') ?? 0; // Mengambil user_id, default 0 jika tidak ada + + if (userId == 0) { + // Jika user_id tidak valid, tampilkan pesan error + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('User ID tidak ditemukan.'), + )); + return; + } + + // Panggil API untuk mengambil data progress latihan + final response = await http.get( + Uri.parse('${BaseUrl.baseUrl}/progress/latihan/$userId/${widget.sub_materi_id}'), + headers: {'Authorization': 'Bearer $token'}, // Menambahkan token di header + ); + + if (response.statusCode == 200) { + var data = json.decode(response.body); + setState(() { + progressData = data['data']; // Ambil data progress list dari respons + totalNilai = (data['data'][0]['total_nilai'] ?? 0.0).toInt(); // Pastikan total_nilai adalah int + status = totalNilai >= 70 ? "Selesai" : "Gagal"; // Tentukan status berdasarkan nilai + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + // Handle error jika response tidak OK + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal memuat data validasi'), + )); + } +} + + + @override + Widget build(BuildContext context) { + // Determine the status icon based on total_nilai + Icon statusIcon = totalNilai >= 70 + ? const Icon(Icons.check_circle, color: Colors.green, size: 40) + : const Icon(Icons.cancel, color: Colors.red, size: 40); + + return Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(50), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + ), + margin: EdgeInsets.zero, + child: AppBar( + backgroundColor: + secondPrimaryColor, + title: Text( + 'Hasil Penilaian', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: Colors.white, + onPressed: () { + context.pop(); + }, + ), + ), + ), + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status Icon and Total Nilai + Row( + children: [ + statusIcon, + const SizedBox(width: 10), + Text( + status, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: totalNilai >= 70 ? Colors.green : Colors.red, + ), + ), + ], + ), + const SizedBox(height: 20), + Text( + 'Total Nilai: ${totalNilai.toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 10), + // Displaying progress data + ...progressData.map((progress) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Card displaying Potongan Ayat and Latin Text + Row( + children: [ + const Icon( + Icons.menu_book_rounded, + color: Colors.teal, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + progress['potongan_ayat'] ?? 'Tidak ada data', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.translate, + color: Colors.orange, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + progress['latin_text'] ?? 'Tidak ada data', + style: const TextStyle( + fontSize: 18, + color: Colors.black87, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 10), + Text( + 'pelafalan: ${progress['status_validasi'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 10), + Text( + 'nilai: ${progress['nilai'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 10), + Text( + 'keterangan: ${progress['feedback_pengajar'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + ), + ), + ], + ), + ), + ); + }).toList(), + + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/home/profile/edit_profile.dart b/lib/view/home/profile/edit_profile.dart index 9104ceb..020642c 100644 --- a/lib/view/home/profile/edit_profile.dart +++ b/lib/view/home/profile/edit_profile.dart @@ -24,6 +24,10 @@ class _EditProfileState extends State { String _gender = 'Laki-laki'; bool _isEditing = false; // Track whether we are in edit mode bool _isLoading = true; // Track whether data is loading + String _selectedJenjangPendidikan = 'SD'; // Default value + + // List for dropdown values + List jenjangPendidikanOptions = ['SD', 'SMP', 'SMA', 'Perguruan Tinggi']; @override void initState() { @@ -117,6 +121,21 @@ Future _updateUserData() async { } } +Future _selectDate(BuildContext context) async { + final DateTime? selectedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), // Default current date + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + if (selectedDate != null) { + setState(() { + // Format selected date as 'dd/MM/yyyy' + _dobController.text = "${selectedDate.day}/${selectedDate.month}/${selectedDate.year}"; + }); + } + } + // Show a success dialog after user data is updated successfully void _showSuccessDialog() { @@ -195,9 +214,9 @@ Future _updateUserData() async { ), SizedBox(height: 16), _buildTextFormField(_fullNameController, 'Nama Lengkap', _isEditing), - _buildTextFormField(_addressController, 'Alamat', _isEditing), - _buildTextFormField(_dobController, 'Usia', _isEditing), - _buildTextFormField(_jenjangPendidikanController, 'Jenjang Pendidikan', _isEditing), + _buildAddressField(_addressController, 'Alamat', _isEditing), + _buildDateOfBirthField(_dobController, 'Tanggal Lahir', _isEditing), + _buildDropdownJenjangPendidikan( 'Jenjang Pendidikan', _isEditing), Row( children: [ Text('Jenis Kelamin', style: TextStyle(fontSize: 16)), @@ -242,8 +261,8 @@ Future _updateUserData() async { ), ), SizedBox(height: 16), - _buildTextFormField(_phoneController, 'No WA Wali', _isEditing), - _buildTextFormField(_emailController, 'Email', _isEditing), + _buildPhoneField(_phoneController, 'No WA Wali', _isEditing), + _buildEmailField(_emailController, 'Email', _isEditing), SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20), @@ -275,7 +294,6 @@ Future _updateUserData() async { ); } - // Helper function to build TextFormField Widget _buildTextFormField(TextEditingController controller, String labelText, bool isEnabled) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -284,19 +302,16 @@ Future _updateUserData() async { SizedBox(height: 8), TextFormField( controller: controller, - enabled: isEnabled, // Control whether the field is enabled or not + enabled: isEnabled, + autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input decoration: InputDecoration( filled: true, - fillColor: Colors.white, + fillColor: Colors.grey[200], // Warna latar belakang lebih terang border: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(40), // Sudut melengkung + borderSide: BorderSide.none, // Menghilangkan border default ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blue), - borderRadius: BorderRadius.circular(8), - ), - contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), ), validator: (value) { if (value == null || value.isEmpty) { @@ -309,4 +324,198 @@ Future _updateUserData() async { ], ); } + + Widget _buildAddressField(TextEditingController controller, String labelText, bool isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(labelText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(height: 8), + TextFormField( + controller: _addressController, + keyboardType: TextInputType.multiline, // Membuka multiline pada keyboard + maxLines: 3, // Menentukan tinggi area input, bisa lebih panjang jika diperlukan + autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input + enabled: isEnabled, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, // Menghilangkan border default + ), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Harap masukkan Alamat'; + } + return null; + }, + ), + SizedBox(height: 20), + ], + ); +} + + + + + // Fungsi untuk input tanggal lahir + Widget _buildDateOfBirthField(TextEditingController controller, String labelText, bool isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(labelText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(height: 8), + GestureDetector( + onTap: () => _selectDate(context), + child: AbsorbPointer( + child: TextFormField( + controller: controller, + enabled: isEnabled, + autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(40), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + hintText: 'Pilih Tanggal Lahir', + suffixIcon: Icon(Icons.calendar_today, color: Colors.grey), // Menambahkan ikon kalender + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Harap pilih Tanggal Lahir'; + } + return null; + }, + ), + ), + ), + SizedBox(height: 20), + ], + ); + } + + Widget _buildDropdownJenjangPendidikan(String labelText, bool isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label untuk dropdown + Text(labelText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(height: 8), + + // Dropdown untuk Jenjang Pendidikan + DropdownButtonFormField( + value: _selectedJenjangPendidikan, // Nilai yang dipilih, diambil dari API + onChanged: isEnabled + ? (String? newValue) { + setState(() { + _selectedJenjangPendidikan = newValue!; + }); + } + : null, // Hanya bisa diubah jika dalam mode edit + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(40), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + ), + items: jenjangPendidikanOptions.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Harap pilih Jenjang Pendidikan'; + } + return null; + }, + ), + SizedBox(height: 20), + ], + ); +} + + + // Fungsi untuk input No WA Wali dengan tipe nomor dan ikon di kanan + Widget _buildPhoneField(TextEditingController controller, String labelText, bool isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(labelText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(height: 8), + TextFormField( + controller: controller, + keyboardType: TextInputType.phone, + enabled: isEnabled, // Set keyboard type to phone + autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(40), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + suffixIcon: Icon(Icons.phone, color: Colors.grey), // Icon for phone number on the right + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Harap masukkan No WA Wali'; + } + return null; + }, + ), + SizedBox(height: 20), + ], + ); + } + + // Fungsi untuk input Email dengan tipe email dan ikon di kanan +Widget _buildEmailField(TextEditingController controller, String labelText, bool isEnabled) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(labelText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + SizedBox(height: 8), + TextFormField( + controller: controller, + keyboardType: TextInputType.emailAddress, // Set keyboard type to email + autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input + enabled: isEnabled, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(40), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + suffixIcon: Icon(Icons.email, color: Colors.grey), // Icon for email on the right + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Harap masukkan Email'; + } + // Validasi untuk memastikan email berakhiran @gmail.com + final regex = RegExp(r'^[a-zA-Z0-9._%+-]+@gmail\.com$'); + if (!regex.hasMatch(value)) { + return 'Harap masukkan @gmail.com'; + } + return null; + }, + ), + SizedBox(height: 20), + ], + ); +} } diff --git a/lib/view/home/profile/profile.dart b/lib/view/home/profile/profile.dart index c9b3dbc..0bb33a8 100644 --- a/lib/view/home/profile/profile.dart +++ b/lib/view/home/profile/profile.dart @@ -193,7 +193,7 @@ class _ProfilePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTestResultSection("Usia", _age ?? "-"), + _buildTestResultSection("Tanggal Lahir", _age ?? "-"), Divider(color: Colors.white), _buildTestResultSection("Email", _email ?? "-"), Divider(color: Colors.white), diff --git a/lib/view/pengajar/data_latihan/data_latihan.dart b/lib/view/pengajar/data_latihan/data_latihan.dart deleted file mode 100644 index 200fe48..0000000 --- a/lib/view/pengajar/data_latihan/data_latihan.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:http/http.dart' as http; -import 'package:ta_tahsin/core/baseurl/base_url.dart'; -import 'package:ta_tahsin/core/theme.dart'; - -class DataLatihanPage extends StatefulWidget { - const DataLatihanPage({super.key}); - - @override - _DataLatihanPageState createState() => _DataLatihanPageState(); -} - -class _DataLatihanPageState extends State { - int _selectedIndex = 0; // To track the selected tab - late Future> kategoriData; - bool isLoading = true; - List materiList = []; - - // Define the content for each "tab" - final List _contentWidgets = [ - Center(child: Text('Konten Materi 1')), - Center(child: Text('Konten Materi 2')), - ]; - - // Function to fetch materi data when a tab is selected - Future _fetchMateri() async { - final response = await http.get(Uri.parse('${BaseUrl.baseUrl}/materi')); - - if (response.statusCode == 200) { - final Map data = json.decode(response.body); - setState(() { - materiList = data['data']; - isLoading = false; - }); - } else { - throw Exception('Failed to load materi'); - } - } - - // Function to fetch kategori data based on the selected materi (id_materi) - Future> fetchKategoriData(int id_materi) async { - final response = await http.get(Uri.parse('${BaseUrl.baseUrl}/kategori/$id_materi')); - - if (response.statusCode == 200) { - setState(() { - isLoading = false; - }); - return json.decode(response.body)['data']; // Fetch categories based on id_materi - } else { - throw Exception('Failed to load kategori'); - } - } - - // Function to fetch sub-materi data based on selected kategori - Future> fetchSubMateriData(int id_kategori) async { - final response = await http.get(Uri.parse('${BaseUrl.baseUrl}/sub_materi/$id_kategori')); - - if (response.statusCode == 200) { - return json.decode(response.body)['data']['sub_materi']; - } else { - throw Exception('Failed to load sub-materi'); - } - } - - // Tab selection callback to update the selected index and fetch relevant kategori data - void _onTabTapped(int index) { - setState(() { - _selectedIndex = index; - int idMateri = index == 0 ? 1 : 2; // 1 for "Makhrijul Huruf" and 2 for "Materi 2" - kategoriData = fetchKategoriData(idMateri); // Fetch kategori based on id_materi - }); - } - - @override - void initState() { - super.initState(); - kategoriData = fetchKategoriData(1); // Default to "Makhrijul Huruf" - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Data Latihan'), - automaticallyImplyLeading: false, - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - // Button Bar with full width buttons - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Materi 1 Button - Expanded( - child: TextButton.icon( - onPressed: () { - _onTabTapped(0); - }, - label: const Text( - 'Makhrijul Huruf', - style: TextStyle( - color: Colors.white, - ), - ), - style: TextButton.styleFrom( - backgroundColor: secondPrimaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 7, - ), - textStyle: const TextStyle(fontSize: 16), - ), - ), - ), - const SizedBox(width: 10), - // Materi 2 Button - Expanded( - child: TextButton.icon( - onPressed: () { - _onTabTapped(1); - }, - label: const Text( - 'Sifatul Huruf', - style: TextStyle( - color: Colors.white, - ), - ), - style: TextButton.styleFrom( - backgroundColor: secondPrimaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 7, - ), - textStyle: const TextStyle(fontSize: 16), - ), - ), - ), - ], - ), - const SizedBox(height: 10,), - // FutureBuilder to fetch and display categories based on selected tab - Expanded( - child: FutureBuilder>( - future: kategoriData, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data == null) { - return Center(child: Text('Tidak ada data tersedia')); - } - - final kategoriList = snapshot.data!; - - return CustomScrollView( - slivers: [ - - // Iterate through categories and fetch corresponding sub-materi - for (var kategori in kategoriList) - FutureBuilder>( - future: fetchSubMateriData(kategori['id']), - builder: (context, subMateriSnapshot) { - if (subMateriSnapshot.connectionState == ConnectionState.waiting) { - return SliverToBoxAdapter(child: SizedBox()); - } else if (subMateriSnapshot.hasError) { - return SliverToBoxAdapter(child: Center(child: Text('Error: ${subMateriSnapshot.error}'))); - } else if (!subMateriSnapshot.hasData || subMateriSnapshot.data == null) { - return SliverToBoxAdapter(child: Center(child: Text('No sub-materi available'))); - } - - final subMateriList = subMateriSnapshot.data!; - - return SliverList( - delegate: SliverChildListDelegate( - [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - child: Text( - kategori['nama_kategori'], - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - - for (var submateri in subMateriList) - Padding( - padding: const EdgeInsets.only(bottom: 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), - leading: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: secondPrimaryColor, - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.menu_book, - color: whiteColor, - size: 24, - ), - ), - title: Row( - children: [ - Expanded( - child: Text( - submateri['title'], - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - ], - ), - subtitle: Text( - submateri['subtitle'], - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - onTap: () { - context.push( - '/detail_data_latihan', - extra: { - 'id': submateri['id'], - }, - ); - }, - ), - Divider( - color: Colors.grey.withOpacity(0.5), - thickness: 1, - indent: 80, - ), - ], - ), - ), - ], - ), - ); - }, - ), - ], - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/view/pengajar/data_latihan/detail_data_latihan.dart b/lib/view/pengajar/data_latihan/detail_data_latihan.dart deleted file mode 100644 index 45b2c29..0000000 --- a/lib/view/pengajar/data_latihan/detail_data_latihan.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class DetailDataLatihanPage extends StatefulWidget { - final int id; - const DetailDataLatihanPage({super.key,required this.id}); - - @override - _DetailDataLatihanPageState createState() => _DetailDataLatihanPageState(); -} - -class _DetailDataLatihanPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Detail Data Latihan'), - automaticallyImplyLeading: false, - ), - body: Center( - child: Text( - 'Halaman Detail Data Latihan', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - ); - } -} diff --git a/lib/view/pengajar/data_latihan/detail_validasi_pelafalan.dart b/lib/view/pengajar/data_latihan/detail_validasi_pelafalan.dart new file mode 100644 index 0000000..17e6a78 --- /dev/null +++ b/lib/view/pengajar/data_latihan/detail_validasi_pelafalan.dart @@ -0,0 +1,584 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ta_tahsin/core/baseurl/base_url.dart'; +import 'package:ta_tahsin/core/theme.dart'; +import 'package:audioplayers/audioplayers.dart'; + +class DetailValidasiPelafalan extends StatefulWidget { + final int user_id; + final int sub_materi_id; + + const DetailValidasiPelafalan({ + super.key, + required this.user_id, + required this.sub_materi_id, + }); + + @override + _DetailValidasiPelafalanState createState() => + _DetailValidasiPelafalanState(); +} + +class _DetailValidasiPelafalanState extends State { + List latihanList = []; + List nilaiList = []; + List statusList = []; + List feedbackList = []; + List feedbackControllers = []; // List untuk TextEditingController + bool isLoading = true; + final AudioPlayer _audioPlayer = AudioPlayer(); + + @override + void initState() { + super.initState(); + fetchProgressLatihanData(); + } + + Future fetchProgressLatihanData() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); + + final response = await http.get( + Uri.parse( + '${BaseUrl.baseUrl}/progress/latihan/${widget.user_id}/${widget.sub_materi_id}', + ), + headers: {'Authorization': 'Bearer $token'}, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + final List fetchedList = data['data'] ?? []; + + setState(() { + latihanList = fetchedList; + nilaiList = List.filled(fetchedList.length, 0); + statusList = List.filled(fetchedList.length, null); + feedbackList = List.filled(fetchedList.length, null); + feedbackControllers = List.generate( + fetchedList.length, + (index) => TextEditingController(text: feedbackList[index] ?? ''), + ); + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + debugPrint('Failed to load latihan data'); + } + } + + void playRecordedAudio(String audioUrl) async { + try { + await _audioPlayer.play(UrlSource(audioUrl)); + debugPrint("Audio is playing: $audioUrl"); + } catch (e) { + debugPrint("Failed to play audio: $e"); + } + } + + // Function to call the API to save penilaian + Future savePenilaian() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); + + // Prepare the data to send in the POST request + List idProgressList = []; + List statusValidasiList = []; + List feedbackPengajarList = []; + List nilaiListToSend = []; + + for (int i = 0; i < latihanList.length; i++) { + if (statusList[i] != null) { // Only send records that are updated + // Ensure 'id' is not null and provide a default value if null + var latihanId = latihanList[i]['id_progress']; + if (latihanId == null) { + debugPrint('Warning: latihanList[$i]["id"] is null. Using default value 0.'); + latihanId = 0; // Assign default value if null + } + + idProgressList.add(latihanId); // Add the ID + statusValidasiList.add(statusList[i]); + feedbackPengajarList.add(feedbackList[i]); + + // Ensure `nilai` is not null and default to 0 if null + int nilai = nilaiList[i] ?? 0; // Use 0 if nilaiList[i] is null + nilaiListToSend.add(nilai); + } + } + + final response = await http.post( + Uri.parse('${BaseUrl.baseUrl}/save_penilaian'), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'id_progress': idProgressList, + 'status_validasi': statusValidasiList, + 'feedback_pengajar': feedbackPengajarList, + 'nilai': nilaiListToSend, + }), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == true) { + // Successfully saved penilaian + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(data['message']), + )); + } else { + // Error saving penilaian + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(data['message']), + )); + } + } else { + // Handle API error + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Failed to save penilaian'), + )); + } +} + + + + + @override + void dispose() { + // Dispose semua controller untuk feedback + for (var controller in feedbackControllers) { + controller.dispose(); + } + _audioPlayer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final title = latihanList.isNotEmpty ? latihanList[0]['title'] ?? '' : ''; + final subtitle = + latihanList.isNotEmpty ? latihanList[0]['subtitle'] ?? '' : ''; + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Card( + elevation: 4, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: EdgeInsets.zero, + child: AppBar( + backgroundColor: secondPrimaryColor, + title: const Text( + 'Validasi Pelafalan', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + color: Colors.white, + onPressed: () => context.pop(), + ), + ), + ), + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : latihanList.isEmpty + ? const Center(child: Text('Tidak ada data latihan')) + : Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + subtitle, + style: const TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 20), + Expanded( + child: ListView.builder( + itemCount: latihanList.length, + itemBuilder: (context, index) { + var item = latihanList[index]; + final recorderPath = item['recorder_audio']; + final audioUrl = + '${BaseUrl.audioUrl}/storage/${recorderPath ?? ''}'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Card Ayat & Audio + Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.15), + spreadRadius: 2, + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.menu_book_rounded, + color: Colors.teal, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item['potongan_ayat'] ?? '-', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.translate, + color: Colors.orange, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item['latin_text'] ?? '-', + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.volume_up_rounded, + color: secondPrimaryColor, + ), + const SizedBox(width: 10), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: + secondPrimaryColor, + foregroundColor: whiteColor, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + icon: const Icon(Icons.play_arrow), + label: const Text("Putar Audio"), + onPressed: () { + if (recorderPath != null && + recorderPath.isNotEmpty) { + playRecordedAudio(audioUrl); + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + "Audio tidak tersedia", + ), + ), + ); + } + }, + ), + ], + ), + ], + ), + ), + ), + + // Tombol Benar / Salah + Padding( + padding: const EdgeInsets.only( + bottom: 12.0, + left: 4.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + statusList[index] = 'benar'; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: statusList.length > index && + statusList[index] == 'salah' + ? Colors.grey + : Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text("Benar"), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: () { + setState(() { + statusList[index] = 'salah'; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: statusList.length > index && + statusList[index] == 'benar' + ? Colors.grey + : Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text("Salah"), + ), + ], + ), + ), + // Container for Penilaian -> only shows when status is selected + if (statusList.length > index && statusList[index] != null) + Container( + margin: const EdgeInsets.only(bottom: 24.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 8, + spreadRadius: 1, + offset: const Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon( + Icons.feedback_outlined, + color: Colors.blueAccent, + ), + SizedBox(width: 8), + Text( + "Penilaian Pengajar", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blueAccent, + ), + ), + ], + ), + const Divider( + height: 24, + color: Colors.grey, + ), + const Text( + "📝 Keterangan / Feedback", + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + TextField( + controller: feedbackControllers[index], // Menggunakan controller per index + decoration: InputDecoration( + hintText: "Tulis feedback...", + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(12), + ), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + setState(() { + feedbackList[index] = value; + }); + }, + maxLines: 3, + ), + const SizedBox(height: 16), + const Text( + "📊 Nilai (0-100)", + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + IconButton( + icon: const Icon( + Icons.remove_circle, + color: Colors.redAccent, + ), + onPressed: () { + setState(() { + // Only decrease nilai if status is "benar" + if (statusList[index] == 'benar' && + nilaiList[index] >= 10) { + nilaiList[index] -= 10; + } + }); + }, + ), + Container( + width: 60, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(8), + color: Colors.grey.shade100, + ), + child: Text( + nilaiList.length > index + ? nilaiList[index].toString() + : '0', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.add_circle, + color: Colors.green, + ), + onPressed: () { + setState(() { + // Only increase nilai if status is "benar" + if (statusList[index] == 'benar' && + nilaiList[index] <= 90) { + nilaiList[index] += 10; + } + }); + }, + ), + ], + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + // Save Button + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Center( + child: ElevatedButton( + onPressed: () { + savePenilaian(); + // Navigasi ke halaman hasil_validasi.dart dengan data +context.push( + '/hasil_validasi', + extra: { + 'user_id': widget.user_id, + 'sub_materi_id': widget.sub_materi_id, + }, +); + + }, + style: ElevatedButton.styleFrom( + backgroundColor: secondPrimaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 12.0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), // Rounded corners + ), + ).copyWith( + splashFactory: InkRipple.splashFactory, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.save_alt, + color: whiteColor, + ), + const SizedBox(width: 8), + Text( + "Simpan", + style: TextStyle( + color: whiteColor, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/view/pengajar/data_latihan/hasil_validasi.dart b/lib/view/pengajar/data_latihan/hasil_validasi.dart new file mode 100644 index 0000000..63c44c2 --- /dev/null +++ b/lib/view/pengajar/data_latihan/hasil_validasi.dart @@ -0,0 +1,254 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ta_tahsin/core/baseurl/base_url.dart'; +import 'package:ta_tahsin/core/theme.dart'; + +class HasilValidasiPage extends StatefulWidget { + final int user_id; + final int sub_materi_id; + + const HasilValidasiPage({ + super.key, + required this.user_id, + required this.sub_materi_id, + }); + + @override + _HasilValidasiPageState createState() => _HasilValidasiPageState(); +} + +class _HasilValidasiPageState extends State { + bool isLoading = true; + List progressData = []; + int totalNilai = 0; + String status = ""; + + @override + void initState() { + super.initState(); + fetchProgressData(); + } + + Future fetchProgressData() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); + + final response = await http.get( + Uri.parse('${BaseUrl.baseUrl}/progress/latihan/${widget.user_id}/${widget.sub_materi_id}'), + headers: {'Authorization': 'Bearer $token'}, + ); + + if (response.statusCode == 200) { + var data = json.decode(response.body); + setState(() { + progressData = data['data']; // Ambil data progress list dari respons + totalNilai = (data['data'][0]['total_nilai'] ?? 0.0).toInt(); // Ensure it's an int + status = totalNilai >= 70 ? "Selesai" : "Gagal"; + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + // Handle error here + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal memuat data validasi'), + )); + } +} + + @override + Widget build(BuildContext context) { + // Determine the status icon based on total_nilai + Icon statusIcon = totalNilai >= 70 + ? const Icon(Icons.check_circle, color: Colors.green, size: 40) + : const Icon(Icons.cancel, color: Colors.red, size: 40); + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Card( + elevation: 4, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: EdgeInsets.zero, + child: AppBar( + automaticallyImplyLeading: false, + backgroundColor: secondPrimaryColor, + title: const Text( + 'Hasil Validasi', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status Icon and Total Nilai + Row( + children: [ + statusIcon, + const SizedBox(width: 10), + Text( + status, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: totalNilai >= 70 ? Colors.green : Colors.red, + ), + ), + ], + ), + const SizedBox(height: 20), + Text( + 'Total Nilai: ${totalNilai.toStringAsFixed(2)}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 10), + // Displaying progress data + ...progressData.map((progress) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Card displaying Potongan Ayat and Latin Text + Row( + children: [ + const Icon( + Icons.menu_book_rounded, + color: Colors.teal, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + progress['potongan_ayat'] ?? 'Tidak ada data', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.translate, + color: Colors.orange, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + progress['latin_text'] ?? 'Tidak ada data', + style: const TextStyle( + fontSize: 18, + color: Colors.black87, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 10), + Text( + 'pelafalan: ${progress['status_validasi'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 10), + Text( + 'nilai: ${progress['nilai'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 10), + Text( + 'keterangan: ${progress['feedback_pengajar'] ?? 'Tidak ada data'}', + style: const TextStyle( + fontSize: 18, + ), + ), + ], + ), + ), + ); + }).toList(), + // Adding "Selesai" button at the bottom + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Center( + child: ElevatedButton( + onPressed: () { + context.go('/navigasiPengajar'); + + }, + style: ElevatedButton.styleFrom( + backgroundColor: secondPrimaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 12.0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), // Rounded corners + ), + ).copyWith( + splashFactory: InkRipple.splashFactory, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "selesai", + style: TextStyle( + color: whiteColor, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ], + ), + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/pengajar/data_latihan/validasi_pelafalan.dart b/lib/view/pengajar/data_latihan/validasi_pelafalan.dart new file mode 100644 index 0000000..c3e7055 --- /dev/null +++ b/lib/view/pengajar/data_latihan/validasi_pelafalan.dart @@ -0,0 +1,248 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ta_tahsin/core/baseurl/base_url.dart'; +import 'package:ta_tahsin/core/theme.dart'; + +class ValidasiPelafalan extends StatefulWidget { + const ValidasiPelafalan({super.key}); + + @override + _ValidasiPelafalanState createState() => _ValidasiPelafalanState(); +} + +class _ValidasiPelafalanState extends State { + List santriList = []; + List filteredSantriList = []; + String searchQuery = ""; + bool isLoading = true; + + @override + void initState() { + super.initState(); + fetchProgressMenunggu(); + } + + Future fetchProgressMenunggu() async { + setState(() { + isLoading = true; + }); + + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); + + final response = await http.get( + Uri.parse('${BaseUrl.baseUrl}/progress/menunggu'), + headers: {'Authorization': 'Bearer $token', 'Accept': 'application/json'}, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + setState(() { + santriList = data['data']; + filteredSantriList = santriList; + isLoading = false; + }); + } else { + print("Gagal memuat data: ${response.statusCode}"); + setState(() { + isLoading = false; + }); + } + } + + void filterSantri(String query) { + setState(() { + filteredSantriList = + santriList + .where( + (santri) => santri['nama_lengkap'].toLowerCase().contains( + query.toLowerCase(), + ), + ) + .toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Penilaian'), + automaticallyImplyLeading: false, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + onChanged: (query) { + setState(() { + searchQuery = query; + filterSantri(query); + }); + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: 'Cari Santri...', + hintStyle: const TextStyle(color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: secondPrimaryColor), + ), + ), + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Text( + 'Pilih Santri', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: secondPrimaryColor, + ), + ), + ), + + const SizedBox(height: 10), + Expanded( + child: + isLoading + ? const Center(child: CircularProgressIndicator()) + : filteredSantriList.isEmpty + ? const Center(child: Text('Data tidak ditemukan')) + : CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate(( + context, + index, + ) { + var santri = filteredSantriList[index]; + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ListTile( + contentPadding: + const EdgeInsets.symmetric( + vertical: 5.0, + ), + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: AssetImage( + 'assets/icon/defaultprofile.jpeg', + ), + fit: BoxFit.cover, + ), + ), + ), + title: Row( + children: [ + Expanded( + child: Text( + santri['nama_lengkap'] ?? + '', + style: const TextStyle( + fontSize: 16, + fontWeight: + FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + santri['title'] ?? '', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + Text( + santri['subtitle'] ?? '', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + onTap: () { + final idUser = santri['user_id']; + final subMateriId = + santri['sub_materi_id']; + print( + "user_id: ${santri['user_id']}", + ); + print( + "sub_materi_id: ${santri['sub_materi_id']}", + ); + + if (idUser != null && + subMateriId != null) { + context.push( + '/detail_validasi', + extra: { + 'user_id': idUser, + 'sub_materi_id': + subMateriId, + }, + ); + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + "Data tidak lengkap untuk validasi", + ), + ), + ); + } + }, + ), + Divider( + color: Colors.grey.withOpacity(0.5), + thickness: 1, + indent: 80, + ), + ], + ), + ), + ], + ), + ); + }, childCount: filteredSantriList.length), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/pengajar/kemajuan/kemajuan.dart b/lib/view/pengajar/kemajuan/kemajuan.dart index 3f85a97..3d747ec 100644 --- a/lib/view/pengajar/kemajuan/kemajuan.dart +++ b/lib/view/pengajar/kemajuan/kemajuan.dart @@ -71,19 +71,48 @@ class _KemajuanPageState extends State { // Fungsi untuk mengambil data kemajuan Future fetchKemajuanData() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - String? token = prefs.getString('token'); + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); - try { - final response = await http.get( - Uri.parse('${BaseUrl.baseUrl}/users/progres'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); + try { + final response = await http.get( + Uri.parse('${BaseUrl.baseUrl}/users/progres'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); - if (response.statusCode == 200) { - var data = json.decode(response.body); + if (response.statusCode == 200) { + var data = json.decode(response.body); + + if (data['data'] == null || (data['data'] as List).isEmpty) { + // If no data found, show a warning message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning, color: Colors.white), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Tidak ada santri yang menyelesaikan latihan.', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + backgroundColor: Colors.orange, // Set the background color for warning + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ), + ), + ), + ); + } else { setState(() { kemajuanList = data['data']; // Menyimpan data kemajuan filteredKemajuanList = kemajuanList; @@ -97,24 +126,71 @@ class _KemajuanPageState extends State { loadedUserIds.add(kemajuan['user_id']); // Tandai user_id yang sudah dimuat } } - } else { - setState(() { - isLoading = false; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to fetch data: ${response.body}')), - ); } - } catch (e) { + } else { setState(() { isLoading = false; }); - print('Error occurred: $e'); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error occurred: $e')), + SnackBar( + content: Row( + children: [ + const Icon(Icons.error, color: Colors.white), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Gagal memuat data kemajuan: ${response.body}', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + backgroundColor: Colors.red, // Set the background color for error + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ), + ), + ), ); } + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error occurred: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.error, color: Colors.white), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Terjadi kesalahan saat mengambil data: $e', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + backgroundColor: Colors.red, // Set the background color for error + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ), + ), + ), + ); } +} + + // Fungsi untuk mengambil data progres berdasarkan userId Future fetchProgressData(int userId) async {