23/06/25 fitur validasi pelafalan santri belum

This commit is contained in:
vionar3 2025-06-23 16:16:06 +07:00
parent e0b5c274b3
commit 4d896d6628
25 changed files with 3853 additions and 730 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

View File

@ -1,4 +1,4 @@
// lib/config.dart
class BaseUrl {
static const String baseUrl = 'http://192.168.0.102:8000/api';
static const String baseUrl = 'https://legal-marginally-macaque.ngrok-free.app/api';
}

View File

@ -24,7 +24,7 @@ class _NavigationPengajarPageState extends State<NavigationPengajarPage> {
final List<Widget> _pages = [
const KemajuanPage(),
const DataSantriPage(),
const DataLatihanPage(),
// const DataLatihanPage(),
const PengajarProfilePage(),
];
@ -52,10 +52,10 @@ class _NavigationPengajarPageState extends State<NavigationPengajarPage> {
icon: Icon(Icons.list_alt),
label: "Data Santri",
),
BottomNavigationBarItem(
icon: Icon(Icons.list_alt),
label: "Data Latihan",
),
// BottomNavigationBarItem(
// icon: Icon(Icons.list_alt),
// label: "Data Latihan",
// ),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: "Profile",

View File

@ -1,13 +1,18 @@
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/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_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';
import 'package:ta_tahsin/view/pengajar/profile/edit_profile_pengajar.dart';
import 'package:ta_tahsin/view/pengajar/profile/ubah_password_pengajar.dart';
import '../../view/auth/login/login.dart';
import '../../view/home/latihan/pelafalan_popup.dart';
@ -19,10 +24,10 @@ import '../navigation/navigation.dart';
import '../navigation/navigation_pengajar.dart';
final router = GoRouter(
initialLocation: '/login',
// initialLocation: '/login',
routes: [
GoRoute(
path: '/login',
path: '/',
builder: (context, state) => const LoginPage(),
),
GoRoute(
@ -94,14 +99,22 @@ GoRoute(
},
),
GoRoute(
path: '/detail_kemajuan',
builder: (BuildContext context, GoRouterState state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
return DetailKemajuanPage(
nama: extra['nama'],
);
},
),
path: '/detail_kemajuan',
builder: (BuildContext context, GoRouterState state) {
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
// Mengambil data yang diteruskan (nama dan user_id)
final String nama = extra['nama'];
final int userId = extra['user_id'];
// Mengirimkan data ke halaman DetailKemajuanPage
return DetailKemajuanPage(
nama: nama,
userId: userId,
);
},
),
GoRoute(
path: '/data_santri',
builder: (context, state) {
@ -152,5 +165,40 @@ GoRoute(
);
},
),
GoRoute(
path: '/progres_belajar',
builder: (BuildContext context, GoRouterState state) {
return ProgresBelajarPage(
);
},
),
GoRoute(
path: '/ubah_password',
builder: (BuildContext context, GoRouterState state) {
return ChangePasswordPage(
);
},
),
GoRoute(
path: '/ubah_password_pengajar',
builder: (BuildContext context, GoRouterState state) {
return ChangePasswordPengajarPage(
);
},
),
GoRoute(
path: '/edit_profile',
builder: (BuildContext context, GoRouterState state) {
return EditProfile(
);
},
),
GoRoute(
path: '/edit_profile_pengajar',
builder: (BuildContext context, GoRouterState state) {
return EditProfilePengajar(
);
},
),
],
);

View File

@ -0,0 +1,235 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
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:http/http.dart' as http;
import 'dart:convert';
class ChangePasswordPage extends StatefulWidget {
const ChangePasswordPage({super.key});
@override
_ChangePasswordPageState createState() => _ChangePasswordPageState();
}
class _ChangePasswordPageState extends State<ChangePasswordPage> {
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
String? _errorMessage;
bool showNewPassword = true;
bool showConfirmPassword = true;
// Update visibility for New Password field
void updateNewPasswordVisibility() {
setState(() {
showNewPassword = !showNewPassword;
});
}
// Update visibility for Confirm Password field
void updateConfirmPasswordVisibility() {
setState(() {
showConfirmPassword = !showConfirmPassword;
});
}
Future<void> handleChangePassword() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
// Validasi panjang password minimal 8 karakter
if (_newPasswordController.text.length < 8) {
setState(() {
_errorMessage = 'Password harus minimal 8 karakter';
});
return;
}
// Validasi jika password baru dan konfirmasi password tidak cocok
if (_newPasswordController.text != _confirmPasswordController.text) {
setState(() {
_errorMessage = 'Password baru dan konfirmasi password tidak cocok';
});
return;
}
setState(() {
_errorMessage = null; // Reset error message
});
// URL API untuk mengubah password
String url = '${BaseUrl.baseUrl}/change_password'; // Ganti dengan URL API Anda
debugPrint("URL yang digunakan: $url");
// Data yang akan dikirim
Map<String, dynamic> requestBody = {
'new_password': _newPasswordController.text,
'new_password_confirmation': _confirmPasswordController.text,
};
debugPrint("Request Body: $requestBody");
// Kirim request ke API
final response = await http.post(
Uri.parse(url),
headers: {
'Authorization': 'Bearer $authToken',
'Content-Type': 'application/json', // Pastikan header ini ada
},
body: jsonEncode(requestBody),
);
debugPrint("Status code: ${response.statusCode}");
debugPrint("Response body: ${response.body}");
if (response.statusCode == 200) {
// Jika berhasil, tampilkan pesan sukses
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Password berhasil diubah')),
);
context.go('/navigasi'); // Kembali ke halaman sebelumnya
} else {
// Jika gagal, tampilkan pesan error dari response body
final responseBody = json.decode(response.body);
setState(() {
// Menampilkan pesan error jika ada
_errorMessage = responseBody['message']['new_password']?.first ??
'Terjadi kesalahan, coba lagi';
});
}
}
@override
Widget build(BuildContext context) {
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(
"Ubah Password",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () {
context.go('/navigasi');
},
),
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 50.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Password Input Field
Text(
'Password Baru',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
TextField(
controller: _newPasswordController,
obscureText: showNewPassword,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
showNewPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateNewPasswordVisibility,
),
filled: true,
fillColor: Colors.grey[200], // Lighter background for the field
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide.none, // Remove default border
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
),
const SizedBox(height: 16),
// Confirm Password Input Field
Text(
'Konfirmasi Password',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
TextField(
controller: _confirmPasswordController,
obscureText: showConfirmPassword,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
showConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateConfirmPasswordVisibility,
),
filled: true,
fillColor: Colors.grey[200], // Lighter background for the field
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide.none, // Remove default border
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
),
const SizedBox(height: 16),
// Error Message
if (_errorMessage != null)
Text(
_errorMessage!,
style: TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 40),
// Send Button
ElevatedButton(
onPressed: () {
handleChangePassword();
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
minimumSize: Size(double.infinity, 40),
),
child: Text(
'Selesai',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ta_tahsin/core/baseurl/base_url.dart';
@ -21,6 +19,9 @@ class _LoginPageState extends State<LoginPage> {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool showPass = true;
bool isLoading = false; // Menambahkan variabel untuk status loading
bool isEmailEmpty = false; // Validasi untuk email
bool isPasswordEmpty = false; // Validasi untuk password
void updateObsecure() {
setState(() {
@ -29,152 +30,214 @@ class _LoginPageState extends State<LoginPage> {
}
Future<void> login() async {
setState(() {
isEmailEmpty = emailController.text.isEmpty;
isPasswordEmpty = passwordController.text.isEmpty;
});
// Cek jika ada input yang kosong
if (isEmailEmpty || isPasswordEmpty) {
return; // Jika ada input yang kosong, hentikan proses login
}
setState(() {
isLoading = true; // Menandakan bahwa login sedang diproses
});
final response = await http.post(
Uri.parse('${BaseUrl.baseUrl}/login'),
Uri.parse('${BaseUrl.baseUrl}/loginWithTelp'),
body: {
'email': emailController.text,
'no_telp_wali': emailController.text,
'password': passwordController.text,
},
);
setState(() {
isLoading = false; // Menandakan bahwa login telah selesai
});
if (response.statusCode == 200) {
var data = json.decode(response.body);
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('token', data['data']['access_token']);
String peran = data['data']['user']['peran'];
prefs.setString('peran', peran);
if (peran == 'santri') {
router.push("/navigasi");
router.push("/navigasi");
} else if (peran == 'pengajar') {
router.push("/navigasiPengajar");
router.push("/navigasiPengajar");
}
} else {
// debugPrint("anjing");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login Failed: ${response.body}')),
);
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white), // Menambahkan ikon error
const SizedBox(width: 10),
Expanded(
child: Text(
'${json.decode(response.body)['data']['message']}',
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: alertTextColor, // Mengubah warna latar belakang menjadi merah
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), // Menambahkan radius hanya di atas kiri
topRight: Radius.circular(10), // Menambahkan radius hanya di atas kanan
bottomLeft: Radius.zero, // Tidak ada radius di bawah kiri
bottomRight: Radius.zero, // Tidak ada radius di bawah kanan
),
),
),
);
}
}
void showErrorBottomSheet(BuildContext context, String message) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent, // Menjadikan background transparan
builder: (BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.red, // Mengubah warna latar belakang menjadi merah
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
const Icon(Icons.error, color: Colors.white), // Menambahkan ikon error
const SizedBox(width: 10),
Expanded(
child: Text(
message, // Menampilkan pesan error
style: const TextStyle(color: Colors.white),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: PaddingCustom().paddingHorizontal(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
"Selamat Datang,",
style: TextStyle(
fontSize: 26,
fontWeight: bold,
color: blackColor,
),
),
const SizedBox(height: 5),
const Text(
"Masuk Untuk Melanjutkan",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 20),
Center(child: Image.asset('assets/logo/sho.jpg', height: 180)),
const SizedBox(height: 20),
const Text(
"Email",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 5),
TextField(
controller: emailController,
keyboardType:
TextInputType
.emailAddress,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.email,
color: Colors.grey,
),
hintText:
"Masukkan Email",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 15),
const Text(
"Password",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 5),
TextField(
controller: passwordController,
obscureText: showPass,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.grey),
hintText: "Enter your password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: IconButton(
icon: Icon(
showPass ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateObsecure,
),
),
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
},
child: Text(
"Lupa password?",
style: TextStyle(color: secondPrimaryColor, fontSize: 14),
),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
login();
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
foregroundColor: whiteColor,
padding: PaddingCustom().paddingVertical(15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("Masuk"),
),
),
const SizedBox(height: 30),
],
body: Column(
children: [
// Top half: Background image
Container(
height: MediaQuery.of(context).size.height * 0.5, // Half the screen height
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/muhajirin4.jpg'), // Your image
fit: BoxFit.cover,
),
),
),
),
// Bottom half: Form section
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
color: Colors.white, // White background for the form
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"No Telp",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 5),
TextField(
controller: emailController, // Ubah menjadi controller untuk no_telp jika diperlukan
keyboardType: TextInputType.phone, // Gunakan TextInputType.phone untuk nomor telepon
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.phone, // Ganti icon dengan ikon telepon
color: Colors.grey,
),
hintText: "Masukkan No. Telepon", // Ubah hint text sesuai kebutuhan
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
errorText: isEmailEmpty ? 'No Telepon tidak boleh kosong' : null, // Menambahkan pesan error
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: isEmailEmpty ? Colors.red : Colors.blue), // Ganti warna border saat error
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 15),
const Text(
"Password",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 5),
TextField(
controller: passwordController,
obscureText: showPass,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.grey),
hintText: "Masukkan Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: IconButton(
icon: Icon(
showPass ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateObsecure,
),
errorText: isPasswordEmpty ? 'Password tidak boleh kosong' : null, // Menambahkan pesan error
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: isPasswordEmpty ? Colors.red : Colors.blue), // Ganti warna border saat error
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : () { // Disable button saat loading
login();
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
foregroundColor: whiteColor,
padding: PaddingCustom().paddingVertical(15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: isLoading
? CircularProgressIndicator(color: whiteColor) // Menampilkan indikator loading
: const Text("Masuk"), // Menampilkan teks "Masuk" ketika tidak loading
),
),
const SizedBox(height: 30),
],
),
),
),
),
],
),
);
}
}
}

View File

@ -5,12 +5,14 @@ import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart';
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:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:http_parser/http_parser.dart';
class LatihanPage extends StatefulWidget {
final int id;
@ -101,14 +103,134 @@ class _LatihanPageState extends State<LatihanPage> {
}
}
// Fungsi untuk menghentikan perekaman
Future<void> stopRecording() async {
await _record.stop();
// // Fungsi untuk menghentikan perekaman
// Future<void> stopRecording() async {
// await _record.stop();
// setState(() {
// isRecording = false;
// });
// }
// // In stopRecording(), save the recorded audio file path to the database
// Future<void> 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
// }
// }
Future<void> uploadRecording(String filePath) 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);
// 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
);
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();
if (response.statusCode == 200) {
print('Upload berhasil!');
} else {
print('Gagal mengupload file: ${response.statusCode}');
}
}
Future<void> stopRecording() async {
await _record.stop();
await uploadRecording(recordedFilePath!);
// Check if the widget is still mounted before calling setState
if (mounted) {
setState(() {
isRecording = false;
});
}
// Get the latihan id from the latihanData list using the current step
final latihanList = await latihanData; // Fetch the data again if not already fetched
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');
debugPrint('File Path: $recordedFilePath');
// 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
}
}
Future<void> saveRecordedAudioName(int idLatihan, String filePath) 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<void> storeLatihanId(int idLatihan) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<int> latihanIds = prefs.getStringList('latihanIds')?.map((e) => int.parse(e)).toList() ?? [];
latihanIds.add(idLatihan); // 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");
}
void stopTimer() {
countdownTimer.cancel();
@ -340,7 +462,10 @@ class _LatihanPageState extends State<LatihanPage> {
@override
void dispose() {
_record.dispose();
_audioPlayer.dispose();
_audioPlayer.dispose();
if (countdownTimer.isActive) {
countdownTimer.cancel(); // Cancel the timer
}
super.dispose();
}
}

View File

@ -1,6 +1,11 @@
import 'dart:convert';
import 'package:audioplayers/audioplayers.dart';
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 PelafalanPage extends StatefulWidget {
@ -65,6 +70,101 @@ class _PelafalanPageState extends State<PelafalanPage> {
print("Audio playing...");
}
// Future<void> updateProgress(int subMateriId) async {
// SharedPreferences prefs = await SharedPreferences.getInstance();
// String? authToken = prefs.getString('token');
// debugPrint("Token yang diambil: $authToken");
// final String apiUrl = '${BaseUrl.baseUrl}/progress/$subMateriId/update'; // Ganti dengan URL API yang sesuai
// final response = await http.post(
// Uri.parse(apiUrl),
// headers: {
// 'Authorization': 'Bearer $authToken',
// },
// body: jsonEncode({
// 'sub_materi_id': subMateriId, // ID submateri yang sedang dikerjakan
// }),
// );
// if (response.statusCode == 200) {
// // Progres berhasil diupdate
// print('Progress updated successfully');
// // ignore: use_build_context_synchronously
// _showCompletionDialog(context);
// } else {
// // Handle error
// print('Failed to update progress');
// }
// }
Future<void> updateProgress(int submateriId) 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
// Retrieve stored latihan ids from SharedPreferences
List<int> latihanIds = prefs.getStringList('latihanIds')?.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.');
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
}),
);
if (response.statusCode == 200) {
// If successful, show completion dialog
_showCompletionDialog(context);
// After updating, remove the stored latihan IDs from SharedPreferences
await clearLatihanIds();
} else {
// Handle failure response
print('Failed to update progress');
}
}
// Function to remove latihan ids from SharedPreferences after the action
Future<void> clearLatihanIds() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('latihanIds');
debugPrint("Latihan IDs cleared from SharedPreferences.");
}
void _showCompletionDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Selamat!'),
content: Text('Kamu telah menyelesaikan latihan ini.'),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Menutup dialog
context.go('/navigasi');
},
child: Text('OK'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -163,7 +263,7 @@ class _PelafalanPageState extends State<PelafalanPage> {
child: Column(
children: [
Text(
'Pelafalan Ustadz',
'Pelafalan Benar',
style: TextStyle(
color: secondPrimaryColor,
fontWeight: FontWeight.bold,
@ -231,7 +331,9 @@ class _PelafalanPageState extends State<PelafalanPage> {
},
);
} else {
context.go('/navigasi');
// updateProgress(widget.id);
updateProgress(widget.id); // Pass submateri_id (widget.id) and latihan_ids
}
},
style: ElevatedButton.styleFrom(

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'package:ta_tahsin/core/theme.dart';
@ -23,52 +24,70 @@ class MateriPage extends StatefulWidget {
class _MateriPageState extends State<MateriPage> {
late Future<List<dynamic>> kategoriData;
bool isLoading = true;
bool isLoading = true;
@override
void initState() {
super.initState();
kategoriData = fetchKategoriData(widget.id);
kategoriData = fetchKategoriData(widget.id);
}
Future<List<dynamic>> fetchKategoriData(int id_materi) async {
final response = await http.get(Uri.parse('${BaseUrl.baseUrl}/kategori/$id_materi'));
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/kategori/$id_materi'),
);
if (response.statusCode == 200) {
setState(() {
isLoading = false;
isLoading = false;
});
return json.decode(response.body)['data'];
return json.decode(response.body)['data'];
} else {
throw Exception('Failed to load kategori');
}
}
Future<List<dynamic>> fetchSubMateriData(int id_kategori) async {
final response = await http.get(Uri.parse('${BaseUrl.baseUrl}/sub_materi/$id_kategori'));
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'];
return json.decode(response.body)['data']['sub_materi'];
} else {
throw Exception('Failed to load sub-materi');
}
}
Future<String> fetchProgressBySubMateri(int id_submateri) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/progress/$id_submateri/status'),
headers: {'Authorization': 'Bearer $authToken'},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['status']; // Pastikan status yang diterima adalah string
} else {
throw Exception('Failed to load progress');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(50),
preferredSize: Size.fromHeight(50),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
margin: EdgeInsets.zero,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
margin: EdgeInsets.zero,
child: AppBar(
backgroundColor: secondPrimaryColor,
backgroundColor: secondPrimaryColor,
title: Text(
widget.title,
style: TextStyle(
@ -87,26 +106,26 @@ class _MateriPageState extends State<MateriPage> {
),
),
),
body: isLoading
? Center(child: CircularProgressIndicator())
: FutureBuilder<List<dynamic>>(
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'));
}
body:
isLoading
? Center(child: CircularProgressIndicator())
: FutureBuilder<List<dynamic>>(
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!;
final kategoriList = snapshot.data!;
return CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
return CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -123,32 +142,44 @@ class _MateriPageState extends State<MateriPage> {
],
),
),
],
]),
),
),
for (var kategori in kategoriList)
FutureBuilder<List<dynamic>>(
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: 16.0, vertical: 5.0),
for (var kategori in kategoriList)
FutureBuilder<List<dynamic>>(
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(
kategori['nama_kategori'],
'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: 16.0,
vertical: 5.0,
),
child: Text(
kategori['nama_kategori'],
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -156,21 +187,26 @@ class _MateriPageState extends State<MateriPage> {
),
),
),
for (var submateri in subMateriList)
Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
contentPadding:
const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
leading: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(12),
borderRadius:
BorderRadius.circular(12),
),
child: Icon(
Icons.menu_book,
@ -190,20 +226,208 @@ class _MateriPageState extends State<MateriPage> {
),
),
),
FutureBuilder<String>(
future:
fetchProgressBySubMateri(
submateri['id'],
),
builder: (context, snapshot) {
if (snapshot
.connectionState ==
ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot
.hasError) {
return Icon(
Icons.error,
color: Colors.red,
);
} else if (snapshot.hasData) {
final status =
snapshot.data;
debugPrint(
"Status: $status",
);
// return Row(
// mainAxisSize:
// MainAxisSize.min,
// children: [
// if (status == 'selesai')
// Icon(
// Icons
// .check_circle_outline,
// color: Colors.green,
// size: 29,
// ),
// if (status ==
// 'menunggu')
// Icon(
// Icons.pending,
// color:
// Colors.orange,
// size: 29,
// ),
// if (status == 'gagal')
// Icon(
// Icons
// .cancel_outlined,
// color: Colors.red,
// size: 29,
// ),
// ],
// );
return Row(
mainAxisSize:
MainAxisSize.min,
children: [
if (status == 'selesai')
Icon(
Icons
.check_circle_outline,
color: Colors.green,
size: 29,
),
if (status ==
'menunggu')
Icon(
Icons.pending,
color:
Colors.orange,
size: 29,
),
if (status == 'gagal')
Icon(
Icons
.cancel_outlined,
color: Colors.red,
size: 29,
),
// Add "Detail" button for selesai or gagal status
if (status ==
'selesai' ||
status == 'gagal')
Padding(
padding:
const EdgeInsets.only(
left: 10,
),
child: ElevatedButton(
onPressed: () {
debugPrint(
'tapped detail hasil penilaian',
);
},
style: ElevatedButton.styleFrom(
foregroundColor:
Colors
.white,
backgroundColor: secondPrimaryColor,
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 8), // Set text color to white
),
child: Text(
'Hasil',
),
),
),
],
);
}
return SizedBox(); // No icon if status is not 'selesai', 'menunggu', or 'gagal'
},
),
],
),
subtitle: Text(
submateri['subtitle'],
style: const TextStyle(fontSize: 14, color: Colors.grey),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
submateri['subtitle'],
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
FutureBuilder<String>(
future:
fetchProgressBySubMateri(
submateri['id'],
),
builder: (context, snapshot) {
if (snapshot
.connectionState ==
ConnectionState.waiting) {
return SizedBox();
} else if (snapshot
.hasError) {
return SizedBox();
} else if (snapshot.hasData) {
final status =
snapshot.data;
String keterangan = '';
if (status == 'selesai') {
keterangan =
'Telah menyelesaikan latihan';
} else if (status ==
'menunggu') {
keterangan =
'Menunggu dikoreksi oleh guru';
} else if (status ==
'belum selesai') {
keterangan =
'Belum dikerjakan';
} else if (status ==
'gagal') {
keterangan =
'Silahkan diperbaiki';
}
return Padding(
padding:
const EdgeInsets.only(
top: 8.0,
),
child: Text(
keterangan,
style: TextStyle(
fontSize: 14,
color:
status ==
'selesai'
? Colors.green
: (status ==
'menunggu'
? Colors
.orange
: (status ==
'gagal'
? Colors
.red
: secondPrimaryColor)),
fontWeight:
FontWeight.bold,
),
),
);
}
return SizedBox(); // Default return if there's no data
},
),
],
),
onTap: () {
context.push(
'/submateri',
'/submateri',
extra: {
'id': submateri['id'],
'title': submateri['title'],
'description': submateri['subtitle'],
'videoLink': submateri['video_url'],
'description':
submateri['subtitle'],
'videoLink':
submateri['video_url'],
'intro': submateri['intro'],
},
);
@ -217,15 +441,14 @@ class _MateriPageState extends State<MateriPage> {
],
),
),
],
),
);
},
),
],
);
},
),
]),
);
},
),
],
);
},
),
);
}
}

View File

View File

@ -0,0 +1,312 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
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:http/http.dart' as http;
import 'dart:convert';
class EditProfile extends StatefulWidget {
const EditProfile({super.key});
@override
_EditProfileState createState() => _EditProfileState();
}
class _EditProfileState extends State<EditProfile> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _fullNameController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
final TextEditingController _dobController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _jenjangPendidikanController = TextEditingController();
String _gender = 'Laki-laki';
bool _isEditing = false; // Track whether we are in edit mode
bool _isLoading = true; // Track whether data is loading
@override
void initState() {
super.initState();
_fetchUserData();
}
// Fetch user data from API
Future<void> _fetchUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
setState(() {
_isLoading = false;
});
return;
}
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/user'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_fullNameController.text = data['data']['nama_lengkap'];
_addressController.text = data['data']['alamat'];
_dobController.text = data['data']['usia'];
_phoneController.text = data['data']['no_telp_wali'];
_emailController.text = data['data']['email'];
_jenjangPendidikanController.text = data['data']['jenjang_pendidikan'];
_gender = data['data']['jenis_kelamin'];
_isLoading = false;
});
} else {
setState(() {
_isLoading = false;
});
}
}
// Update user data using API
Future<void> _updateUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Token tidak ditemukan')),
);
return;
}
final response = await http.post(
Uri.parse('${BaseUrl.baseUrl}/user/updateBytoken'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'nama_lengkap': _fullNameController.text,
'alamat': _addressController.text,
'usia': _dobController.text,
'no_telp_wali': _phoneController.text,
'email': _emailController.text,
'jenjang_pendidikan': _jenjangPendidikanController.text,
'jenis_kelamin': _gender,
}),
);
// Print response status and body for debugging
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
if (response.statusCode == 200) {
_showSuccessDialog();
setState(() {
_isEditing = false;
});
// Optionally navigate to another page or refresh profile
} else {
// Handle error and print response body
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal mengubah data: ${response.body}')),
);
print("Error response: ${response.body}"); // Print error response here
}
}
// Show a success dialog after user data is updated successfully
void _showSuccessDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Berhasil'),
content: Text('Data santri berhasil diubah.'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
setState(() {
_isEditing = false;
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Profile'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
context.go('/navigasi');
},
),
actions: [
IconButton(
icon: Icon(Icons.edit_note, size: 35),
onPressed: () {
setState(() {
_isEditing = !_isEditing; // Toggle edit mode
});
},
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: CircleAvatar(
radius: 60,
backgroundImage: AssetImage('assets/icon/defaultprofile.jpeg'),
backgroundColor: Colors.transparent,
),
),
SizedBox(height: 20),
Container(
width: double.infinity,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, 4))],
),
child: Text(
'Detail Dasar',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: whiteColor),
),
),
SizedBox(height: 16),
_buildTextFormField(_fullNameController, 'Nama Lengkap', _isEditing),
_buildTextFormField(_addressController, 'Alamat', _isEditing),
_buildTextFormField(_dobController, 'Usia', _isEditing),
_buildTextFormField(_jenjangPendidikanController, 'Jenjang Pendidikan', _isEditing),
Row(
children: [
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
Radio<String>(
value: 'Laki-laki',
groupValue: _gender,
onChanged: _isEditing
? (value) {
setState(() {
_gender = value!;
});
}
: null,
),
Text('Laki-laki'),
Radio<String>(
value: 'Perempuan',
groupValue: _gender,
onChanged: _isEditing
? (value) {
setState(() {
_gender = value!;
});
}
: null,
),
Text('Perempuan'),
],
),
SizedBox(height: 20),
Container(
width: double.infinity,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, 4))],
),
child: Text(
'Detail Kontak',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: whiteColor),
),
),
SizedBox(height: 16),
_buildTextFormField(_phoneController, 'No WA Wali', _isEditing),
_buildTextFormField(_emailController, 'Email', _isEditing),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _isEditing ? secondPrimaryColor : Colors.grey, // Button color based on editing
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
elevation: 5, // Adding shadow to the button
),
onPressed: _isEditing
? () {
// Trigger the update user function when editing
_updateUserData();
}
: null,
child: Text(
"Simpan",
style: TextStyle(fontSize: 16, color: whiteColor),
),
),
),
],
),
),
),
);
}
// Helper function to build TextFormField
Widget _buildTextFormField(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,
enabled: isEnabled, // Control whether the field is enabled or not
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Harap masukkan $labelText Anda';
}
return null;
},
),
SizedBox(height: 20),
],
);
}
}

View File

@ -2,45 +2,103 @@ 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 'dart:convert'; // Untuk parsing JSON
import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'package:ta_tahsin/core/theme.dart';
class ProfilePage extends StatelessWidget {
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
String? _name;
String? _email;
String? _age;
String? _phone;
String? _guardianName;
String? _guardianPhone;
String? _address;
String? _gender;
String? _educationLevel;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchUserData();
}
// Fetch user data from API
Future<void> _fetchUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
setState(() {
_errorMessage = "Token tidak ditemukan.";
_isLoading = false;
});
return;
}
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/user'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_name = data['data']['nama_lengkap'];
_email = data['data']['email'];
_age = data['data']['usia'];
_phone = data['data']['no_telp_wali'];
_guardianName = data['data']['nama_wali'];
_guardianPhone = data['data']['no_telp_wali'];
_address = data['data']['alamat'];
_gender = data['data']['jenis_kelamin'];
_educationLevel = data['data']['jenjang_pendidikan'];
_isLoading = false;
});
} else {
setState(() {
_errorMessage = "Gagal mengambil data pengguna: ${response.body}";
_isLoading = false;
});
}
}
// Logout Function
Future<void> logout(BuildContext context) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token != null) {
final response = await http.post(
Uri.parse('${BaseUrl.baseUrl}/logout'),
Uri.parse('${BaseUrl.baseUrl}/logout'),
headers: {
'Authorization': 'Bearer $token',
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
prefs.remove('token');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Berhasil logout!')),
);
context.go('/login');
context.go('/');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal logout: ${response.body}')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Token tidak ditemukan')),
);
@ -52,132 +110,173 @@ class ProfilePage extends StatelessWidget {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(
"Profile",
),
title: Text("Profile"),
automaticallyImplyLeading: false,
),
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 50,
backgroundColor: Colors.white,
backgroundImage: AssetImage('assets/logo/sho.jpg'),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"irfan",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
const SizedBox(height: 5),
Text(
"083166408735",
style: TextStyle(
fontSize: 16, color: Colors.white.withOpacity(0.8)),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [secondPrimaryColor, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.white,
backgroundImage: AssetImage(
'assets/icon/defaultprofile.jpeg'),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
_name ?? "Nama Tidak Ditemukan",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 5),
Text(
_phone ?? "Nomor Telepon Tidak Ditemukan",
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
),
const SizedBox(height: 20),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [secondPrimaryColor, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestResultSection("Usia", _age ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection("Email", _email ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection("Nama Wali",
_guardianName ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Alamat", _address ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Jenis Kelamin", _gender ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Jenjang Pendidikan", _educationLevel ?? "-"),
],
),
),
),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestResultSection("Tgl Lahir", "-"),
Divider(color: Colors.white),
_buildTestResultSection("Alamat", "-"),
Divider(color: Colors.white),
_buildTestResultSection("Nama Orang Tua", "-"),
Divider(color: Colors.white),
_buildTestResultSection("email", "-"),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
),
onPressed: () {
logout(context);
},
child: Text(
"Logout",
style: TextStyle(fontSize: 16,color: whiteColor),
),
),
],
),
),
),
const SizedBox(height: 20),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoLink("Edit Profile", () {
context.go('/edit_profile');
}),
Divider(color: Colors.white),
_buildInfoLink("Progres Belajar", () {
context.go('/progres_belajar');
}),
Divider(color: Colors.white),
_buildInfoLink("Ubah Password", () {
context.go('/ubah_password');
}),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
),
onPressed: () {
logout(context);
},
child: Text(
"Logout",
style: TextStyle(fontSize: 16, color: whiteColor),
),
),
],
),
),
),
),
);
}
Widget _buildTestResultSection(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
@ -188,7 +287,7 @@ class ProfilePage extends StatelessWidget {
label,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SizedBox(height: 6),
SizedBox(height: 6),
Text(
value,
style: TextStyle(fontSize: 16, color: Colors.white),
@ -197,22 +296,25 @@ class ProfilePage extends StatelessWidget {
),
);
}
Widget _buildInfoLink(String label) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
Widget _buildInfoLink(String label, VoidCallback onTap) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: GestureDetector(
onTap: onTap, // Menambahkan aksi saat item ditekan
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Mengatur posisi label dan ikon
children: [
Icon(Icons.arrow_forward, color: secondPrimaryColor),
SizedBox(width: 8),
Text(
label,
style: TextStyle(fontSize: 16, color: secondPrimaryColor),
style: TextStyle(fontSize: 16, color: Colors.white),
),
Icon(
Icons.arrow_forward,
color: Colors.white, // Anda bisa menyesuaikan warna ikon sesuai dengan desain Anda
),
],
),
);
}
),
);
}
}

View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http; // Import the http package
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'dart:convert'; // To parse the JSON data
import 'package:ta_tahsin/core/theme.dart';
class ProgresBelajarPage extends StatefulWidget {
const ProgresBelajarPage({super.key});
@override
_ProgresBelajarPageState createState() => _ProgresBelajarPageState();
}
class _ProgresBelajarPageState extends State<ProgresBelajarPage> {
bool isLoading = true;
double progress = 0.0;
String userName = 'Nama Pengguna'; // Default user name
String userProfileImage = 'assets/icon/defaultprofile.jpeg'; // Default profile image
// Fetch the progress percentage from the API
Future<void> fetchProgressData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/progres/presentase'), // Update with actual API URL
headers: {
'Authorization': 'Bearer $authToken', // If you need to add authentication token
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
progress = data['progress_percentage'] / 100; // Update progress as a fraction
userName = data['nama_lengkap']; // Set user name from API response
isLoading = false; // Set loading to false once data is fetched
});
} else {
// Handle error if the API call fails
setState(() {
isLoading = false;
});
print('Failed to load progress data');
}
}
@override
void initState() {
super.initState();
fetchProgressData(); // Call the function to fetch data when the page is loaded
}
@override
Widget build(BuildContext context) {
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(
"Progres Belajar",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () {
context.go('/navigasi');
},
),
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 50.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Loading Spinner while fetching data
if (isLoading)
Center(child: CircularProgressIndicator())
else ...[
// Foto Profil Pengguna
Center(
child: CircleAvatar(
radius: 50,
backgroundImage: AssetImage('assets/icon/defaultprofile.jpeg'), // Use dynamic profile image
backgroundColor: Colors.transparent,
),
),
const SizedBox(height: 16),
// Nama Pengguna
Center(
child: Text(
userName, // Use dynamic user name
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: blackColor,
),
),
),
const SizedBox(height: 24),
// Judul Progres Belajar
Text(
'Progres Belajar',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: blackColor,
),
),
const SizedBox(height: 10),
// Progress Bar untuk materi
ProgressBar(title: 'Jumlah Latihan Selesai', progress: progress),
const SizedBox(height: 40),
],
],
),
),
),
);
}
}
class ProgressBar extends StatelessWidget {
final String title;
final double progress;
const ProgressBar({super.key, required this.title, required this.progress});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
color: blackColor,
),
),
const SizedBox(height: 10),
Stack(
alignment: Alignment.center,
children: [
// Linear Progress Bar dengan sudut membulat
SizedBox(
height: 30, // Set height of the progress bar
child: ClipRRect(
borderRadius: BorderRadius.circular(15), // Membuat sudut membulat
child: LinearProgressIndicator(
value: progress, // The current progress value
backgroundColor: Colors.grey[300], // Background color
color: secondPrimaryColor, // Progress color
minHeight: 10, // Height of the progress indicator
),
),
),
// Percentage text on top of progress bar
Positioned(
child: Text(
'${(progress * 100).toStringAsFixed(1)}%', // Display percentage
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
],
);
}
}

View File

@ -277,7 +277,7 @@ class _DataSantriPageState extends State<DataSantriPage> {
children: [
TextButton.icon(
onPressed: () {
context.go('/tambah_santri');
context.push('/tambah_santri');
},
icon: const Icon(
Icons.add,
@ -400,7 +400,7 @@ class _DataSantriPageState extends State<DataSantriPage> {
),
),
onTap: () {
context.go('/detail_user', extra: {
context.push('/detail_user', extra: {
'id': santri['id'],
});
// router.push("/detail_user");

View File

@ -24,6 +24,10 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
final TextEditingController _jenjangPendidikanController = TextEditingController();
String _gender = 'Laki-laki';
bool _isEditing = false; // Track whether we are in edit mode
String _selectedJenjangPendidikan = 'SD'; // Default value
// List for dropdown values
List<String> jenjangPendidikanOptions = ['SD', 'SMP', 'SMA', 'Perguruan Tinggi'];
@override
void initState() {
@ -141,6 +145,22 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
);
}
// Function to show DatePicker
Future<void> _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}";
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -149,7 +169,8 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
context.go('/navigasiPengajar');
// context.go('/navigasiPengajar');
context.pop();
},
),
actions: [
@ -192,9 +213,9 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
),
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)),
@ -239,32 +260,33 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
),
),
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),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _isEditing ? secondPrimaryColor : Colors.grey, // Button color based on editing
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
elevation: 5, // Adding shadow to the button
),
onPressed: _isEditing
ElevatedButton(
onPressed: _isEditing
? () {
// Trigger the update user function when editing
_updateUserData();
}
: null,
child: Text(
"Simpan",
style: TextStyle(fontSize: 16, color: whiteColor),
),
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
minimumSize: Size(double.infinity, 40),
),
child: Text(
'Simpan',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
@ -272,7 +294,6 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
);
}
// Helper function to build TextFormField
Widget _buildTextFormField(TextEditingController controller, String labelText, bool isEnabled) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -281,19 +302,16 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
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) {
@ -306,4 +324,198 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
],
);
}
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<String>(
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<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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),
],
);
}
}

View File

@ -15,11 +15,14 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _fullNameController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
final TextEditingController _dobController = TextEditingController();
final TextEditingController _dobController = TextEditingController(); // For DOB
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _jenjangPendidikanController = TextEditingController();
String _gender = 'Laki-laki';
String _selectedJenjangPendidikan = 'SD'; // Default value
// List for dropdown values
List<String> jenjangPendidikanOptions = ['SD', 'SMP', 'SMA', 'Perguruan Tinggi'];
Future<void> _submitForm() async {
if (_formKey.currentState?.validate() ?? false) {
@ -44,11 +47,11 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
final Map<String, String> data = {
'nama_lengkap': _fullNameController.text,
'alamat': _addressController.text,
'usia': _dobController.text,
'usia': _dobController.text, // Use formatted DOB
'no_telp_wali': _phoneController.text,
'email': _emailController.text,
'jenis_kelamin': _gender,
'jenjang_pendidikan': _jenjangPendidikanController.text,
'jenjang_pendidikan': _selectedJenjangPendidikan, // Send selected value
};
final response = await http.post(
@ -74,7 +77,7 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
TextButton(
onPressed: () {
Navigator.of(context).pop();
context.go('/navigasiPengajar');
context.pop();
},
child: Text('OK'),
),
@ -91,7 +94,7 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
actions: <Widget>[
TextButton(
onPressed: () {
context.go('/navigasiPengajar');
context.pop();
},
child: Text('OK'),
),
@ -104,16 +107,51 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
}
}
// Function to show DatePicker
Future<void> _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}";
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tambah Santri'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
context.go('/navigasiPengajar');
},
appBar: PreferredSize(
preferredSize: Size.fromHeight(50),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
margin: EdgeInsets.zero,
child: AppBar(
backgroundColor: secondPrimaryColor,
title: Text(
"Tambah Santri",
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: SingleChildScrollView(
@ -124,9 +162,9 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
child: Column(
children: [
_buildTextFormField(_fullNameController, 'Nama Lengkap'),
_buildTextFormField(_addressController, 'Alamat'),
_buildTextFormField(_dobController, 'Usia'),
_buildTextFormField(_jenjangPendidikanController, 'Jenjang Pendidikan'),
_buildAddressField(_addressController,'Alamat'),
_buildDateOfBirthField(), // Date of Birth Picker
_buildDropdownJenjangPendidikan(), // Jenjang Pendidikan Dropdown
Row(
children: [
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
@ -153,30 +191,31 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
],
),
SizedBox(height: 20),
_buildTextFormField(_phoneController, 'No WA Wali'),
_buildTextFormField(_emailController, 'Email'),
_buildPhoneField(), // No WA Wali input with icon on right
_buildEmailField(), // Email input with icon on right
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
elevation: 5,
),
onPressed: () async {
ElevatedButton(
onPressed: () async {
print("Simpan Taped");
await _submitForm();
},
child: Text(
"Simpan",
style: TextStyle(fontSize: 16, color: whiteColor),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
minimumSize: Size(double.infinity, 40),
),
child: Text(
'Simpan',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
@ -185,7 +224,41 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
);
}
// Fungsi pembuat TextFormField
// Fungsi untuk input Alamat dengan tipe longtext (area teks lebih besar)
Widget _buildAddressField(TextEditingController controller, String labelText) {
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
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 pembuat TextFormField dengan desain seperti pada ubah_password.dart
Widget _buildTextFormField(TextEditingController controller, String labelText) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -194,18 +267,15 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
SizedBox(height: 8),
TextFormField(
controller: controller,
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) {
@ -218,4 +288,155 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
],
);
}
// Fungsi untuk input tanggal lahir
Widget _buildDateOfBirthField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Tanggal Lahir', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
SizedBox(height: 8),
GestureDetector(
onTap: () => _selectDate(context),
child: AbsorbPointer(
child: TextFormField(
controller: _dobController,
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),
],
);
}
// Fungsi untuk dropdown Jenjang Pendidikan
Widget _buildDropdownJenjangPendidikan() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Jenjang Pendidikan', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _selectedJenjangPendidikan,
onChanged: (String? newValue) {
setState(() {
_selectedJenjangPendidikan = newValue!;
});
},
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey[200], // Warna latar belakang lebih terang
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40), // Sudut melengkung
borderSide: BorderSide.none, // Menghilangkan border default
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
items: jenjangPendidikanOptions.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('No WA Wali', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
SizedBox(height: 8),
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone, // 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() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Email', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
SizedBox(height: 8),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress, // Set keyboard type to email
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.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),
],
);
}
}

View File

@ -1,69 +1,212 @@
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 'dart:convert';
import 'package:ta_tahsin/core/theme.dart';
class DetailKemajuanPage extends StatelessWidget {
const DetailKemajuanPage({super.key, required this.nama});
class DetailKemajuanPage extends StatefulWidget {
final String nama;
final int userId;
const DetailKemajuanPage({super.key, required this.nama, required this.userId});
final String nama;
@override
_DetailKemajuanPageState createState() => _DetailKemajuanPageState();
}
class _DetailKemajuanPageState extends State<DetailKemajuanPage> {
bool isLoading = true;
double progress = 0.0;
String userProfileImage = 'assets/icon/defaultprofile.jpeg'; // Default profile image
// Variables to store submateri and completed submateri
int totalSubmateri = 0;
int completedSubmateri = 0;
// Fetch the progress percentage and submateri details from the API
Future<void> fetchProgressData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/progres/${widget.userId}'), // Update with actual API URL
headers: {
'Authorization': 'Bearer $authToken', // If you need to add authentication token
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
progress = data['progress_percentage'] / 100; // Update progress as a fraction
totalSubmateri = data['total_submateri']; // Update total submateri
completedSubmateri = data['completed_submateri']; // Update completed submateri
isLoading = false; // Set loading to false once data is fetched
});
} else {
// Handle error if the API call fails
setState(() {
isLoading = false;
});
print('Failed to load progress data');
}
}
@override
void initState() {
super.initState();
fetchProgressData(); // Call the function to fetch data when the page is loaded
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Detail Kemajuan'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),color: blackColor,
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/navigasiPengajar');
}
},
appBar: PreferredSize(
preferredSize: Size.fromHeight(60),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
margin: EdgeInsets.zero,
child: AppBar(
backgroundColor: secondPrimaryColor,
title: Text(
"Progres Belajar",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.white,
onPressed: () {
context.go('/navigasiPengajar');
},
),
),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Foto Profil Pengguna
Center(
child: CircleAvatar(
radius: 70,
backgroundImage: AssetImage('assets/icon/defaultprofile.jpeg'), // Ganti dengan gambar profil
),
),
const SizedBox(height: 16),
// Nama Pengguna
Center(
child: Text(
nama, // Ganti dengan nama pengguna
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 50.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Loading Spinner while fetching data
if (isLoading)
Center(child: CircularProgressIndicator())
else ...[
// Profile Section with CircleAvatar and Shadow
Center(
child: CircleAvatar(
radius: 60,
backgroundImage: AssetImage(userProfileImage), // Use dynamic profile image
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 3,
blurRadius: 10,
),
],
),
),
),
),
),
),
const SizedBox(height: 40),
// Judul Progres Belajar
Text(
'Progres Belajar',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: blackColor,
),
),
const SizedBox(height: 20),
// Progress Bar untuk materi
ProgressBar(title: 'Jumlah Materi 1 Selesai', progress: 0.6), // Progress 60%
const SizedBox(height: 20),
ProgressBar(title: 'Jumlah Materi 2 Selesai', progress: 0.4), // Progress 40%
],
const SizedBox(height: 16),
// Name Section
Center(
child: Text(
widget.nama, // Use dynamic user name
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: blackColor,
),
),
),
const SizedBox(height: 24),
// Progres Belajar Title
Text(
'Progres Belajar',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: blackColor,
),
),
const SizedBox(height: 10),
// Display total_submateri and completed_submateri in a Card
Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Submateri',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
'$totalSubmateri',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
Divider(),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Completed Submateri',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
'$completedSubmateri',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Progress Bar for learning progress
ProgressBar(title: 'Jumlah Latihan Selesai', progress: progress),
const SizedBox(height: 40),
],
],
),
),
),
);
@ -85,16 +228,38 @@ class ProgressBar extends StatelessWidget {
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: blackColor,
),
),
const SizedBox(height: 5),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[300],
color: secondPrimaryColor, // Ganti dengan warna yang sesuai
minHeight: 20,
const SizedBox(height: 10),
Stack(
alignment: Alignment.center,
children: [
// Linear Progress Bar with rounded corners
SizedBox(
height: 30,
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[300],
color: secondPrimaryColor,
minHeight: 10,
),
),
),
// Percentage text overlaying the progress bar
Positioned(
child: Text(
'${(progress * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
],
);

View File

@ -1,13 +1,167 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:go_router/go_router.dart';
// Pastikan untuk menyesuaikan import theme.dart sesuai lokasi file theme.dart Anda
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:ta_tahsin/view/pengajar/kemajuan/model/model_data_kemajuan.dart'; // Import kemajuanList
class KemajuanPage extends StatelessWidget {
class ProgressBar extends StatelessWidget {
final double progress;
const ProgressBar({super.key, required this.progress});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 5),
Stack(
alignment: Alignment.center,
children: [
// Linear Progress Bar with rounded corners
SizedBox(
height: 20,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[300],
color: secondPrimaryColor,
minHeight: 5,
),
),
),
// Percentage text overlaying the progress bar
Positioned(
child: Text(
'${(progress * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
],
);
}
}
class KemajuanPage extends StatefulWidget {
const KemajuanPage({super.key});
@override
State<KemajuanPage> createState() => _KemajuanPageState();
}
class _KemajuanPageState extends State<KemajuanPage> {
List<dynamic> kemajuanList = []; // Menyimpan list kemajuan
List<dynamic> filteredKemajuanList = []; // Menyimpan list hasil filter
bool isLoading = true; // Menandakan apakah data sedang dimuat
String searchQuery = ""; // Menyimpan query pencarian
// Menyimpan status progres yang sudah dimuat
Set<int> loadedUserIds = Set();
// Fungsi untuk mengambil data kemajuan
Future<void> fetchKemajuanData() async {
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',
},
);
if (response.statusCode == 200) {
var data = json.decode(response.body);
setState(() {
kemajuanList = data['data']; // Menyimpan data kemajuan
filteredKemajuanList = kemajuanList;
isLoading = false;
});
// Setelah data kemajuan berhasil dimuat, panggil fetchProgressData untuk setiap user yang belum dimuat
for (var kemajuan in kemajuanList) {
if (!loadedUserIds.contains(kemajuan['user_id'])) {
fetchProgressData(kemajuan['user_id']);
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) {
setState(() {
isLoading = false;
});
print('Error occurred: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error occurred: $e')),
);
}
}
// Fungsi untuk mengambil data progres berdasarkan userId
Future<void> fetchProgressData(int userId) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/progres/$userId'), // API URL yang sesuai
headers: {
'Authorization': 'Bearer $authToken',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
// Menyimpan progress untuk setiap user
final index = kemajuanList.indexWhere((item) => item['user_id'] == userId);
if (index != -1) {
kemajuanList[index]['progress_percentage'] = data['progress_percentage']; // Menambahkan progres
}
});
} else {
print('Failed to load progress data');
}
}
// Fungsi untuk memfilter data berdasarkan nama
void filterKemajuan(String query) {
final filtered = kemajuanList.where((kemajuan) {
final nama = kemajuan['nama_lengkap'].toLowerCase();
final search = query.toLowerCase();
return nama.contains(search); // Pencarian berdasarkan nama lengkap
}).toList();
setState(() {
filteredKemajuanList = filtered; // Update list yang ditampilkan
});
}
@override
void initState() {
super.initState();
fetchKemajuanData(); // Memanggil fungsi saat halaman pertama kali dibuka
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -22,6 +176,12 @@ class KemajuanPage extends StatelessWidget {
children: [
// Search bar
TextField(
onChanged: (query) {
setState(() {
searchQuery = query;
filterKemajuan(searchQuery);
});
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: 'Cari Santri...',
@ -40,102 +200,108 @@ class KemajuanPage extends StatelessWidget {
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color:
secondPrimaryColor, // Gunakan secondPrimaryColor dari theme.dart
color: secondPrimaryColor,
),
),
const SizedBox(height: 10),
// Menggunakan SliverList dengan kemajuanList
Expanded(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
// Mengambil data kemajuan dari kemajuanList
var kemajuan = kemajuanList[index];
// Jika loading, tampilkan loading indicator
if (isLoading)
Center(child: CircularProgressIndicator())
else if (filteredKemajuanList.isEmpty)
Center(child: Text('Tidak ada progres latihan yang ditemukan.'))
else
// Menggunakan SliverList dengan kemajuanList yang sudah difilter
Expanded(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
var kemajuan = filteredKemajuanList[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Menampilkan detail subMateri, menggunakan subMateri yang terkait
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, // Membuat kontainer berbentuk bulat
image: DecorationImage(
image: AssetImage(
'assets/icon/${kemajuan['image']}',
), // Gambar asset lokal
fit:
BoxFit
.cover, // Gambar akan menyesuaikan dengan ukuran kontainer
),
),
),
title: Row(
children: [
Expanded(
child: Text(
kemajuan['nama'], // Menampilkan nama
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
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,
),
),
],
),
subtitle: Text(
kemajuan['jilid'], // Menampilkan jilid
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
title: Row(
children: [
Expanded(
child: Text(
kemajuan['nama_lengkap'],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kemajuan['no_telp_wali'],
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
// Menggunakan widget ProgressBar
ProgressBar(
progress: kemajuan['progress_percentage'] != null
? kemajuan['progress_percentage'] / 100
: 0.0,
),
],
),
onTap: () {
context.go('/detail_kemajuan', extra: {
'nama': kemajuan['nama_lengkap'],
'user_id': kemajuan['user_id'],
});
},
),
onTap: () {
context.go('/detail_kemajuan',
extra: {'nama': kemajuan['nama']},
);
},
),
Divider(
color: Colors.grey.withOpacity(0.5),
thickness: 1,
indent: 80,
),
],
Divider(
color: Colors.grey.withOpacity(0.5),
thickness: 1,
indent: 40,
),
],
),
),
),
],
),
);
},
childCount:
kemajuanList
.length, // Menyesuaikan jumlah item yang ada pada kemajuanList
],
),
);
},
childCount: filteredKemajuanList.length,
),
),
),
],
],
),
),
),
],
),
),

View File

@ -0,0 +1,522 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
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:http/http.dart' as http;
import 'dart:convert';
class EditProfilePengajar extends StatefulWidget {
const EditProfilePengajar({super.key});
@override
_EditProfilePengajarState createState() => _EditProfilePengajarState();
}
class _EditProfilePengajarState extends State<EditProfilePengajar> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _fullNameController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
final TextEditingController _dobController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _jenjangPendidikanController = TextEditingController();
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<String> jenjangPendidikanOptions = ['SD', 'SMP', 'SMA', 'Perguruan Tinggi'];
@override
void initState() {
super.initState();
_fetchUserData();
}
// Fetch user data from API
Future<void> _fetchUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
setState(() {
_isLoading = false;
});
return;
}
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/user'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_fullNameController.text = data['data']['nama_lengkap'];
_addressController.text = data['data']['alamat'];
_dobController.text = data['data']['usia'];
_phoneController.text = data['data']['no_telp_wali'];
_emailController.text = data['data']['email'];
_jenjangPendidikanController.text = data['data']['jenjang_pendidikan'];
_gender = data['data']['jenis_kelamin'];
_isLoading = false;
});
} else {
setState(() {
_isLoading = false;
});
}
}
// Update user data using API
Future<void> _updateUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Token tidak ditemukan')),
);
return;
}
final response = await http.post(
Uri.parse('${BaseUrl.baseUrl}/user/updateBytoken'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'nama_lengkap': _fullNameController.text,
'alamat': _addressController.text,
'usia': _dobController.text,
'no_telp_wali': _phoneController.text,
'email': _emailController.text,
'jenjang_pendidikan': _jenjangPendidikanController.text,
'jenis_kelamin': _gender,
}),
);
// Print response status and body for debugging
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
if (response.statusCode == 200) {
_showSuccessDialog();
setState(() {
_isEditing = false;
});
// Optionally navigate to another page or refresh profile
} else {
// Handle error and print response body
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal mengubah data: ${response.body}')),
);
print("Error response: ${response.body}"); // Print error response here
}
}
// Show a success dialog after user data is updated successfully
void _showSuccessDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Berhasil'),
content: Text('Data santri berhasil diubah.'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
setState(() {
_isEditing = false;
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
Future<void> _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}";
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Profile Pengajar'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
context.pop();
},
),
actions: [
IconButton(
icon: Icon(Icons.edit_note, size: 35),
onPressed: () {
setState(() {
_isEditing = !_isEditing; // Toggle edit mode
});
},
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: CircleAvatar(
radius: 60,
backgroundImage: AssetImage('assets/icon/defaultprofile.jpeg'),
backgroundColor: Colors.transparent,
),
),
SizedBox(height: 20),
Container(
width: double.infinity,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, 4))],
),
child: Text(
'Detail Dasar',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: whiteColor),
),
),
SizedBox(height: 16),
_buildTextFormField(_fullNameController, 'Nama Lengkap', _isEditing),
_buildAddressField(_addressController, 'Alamat', _isEditing),
_buildDateOfBirthField(_dobController, 'Tanggal Lahir', _isEditing),
_buildDropdownJenjangPendidikan( 'Jenjang Pendidikan', _isEditing),
Row(
children: [
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
Radio<String>(
value: 'Laki-laki',
groupValue: _gender,
onChanged: _isEditing
? (value) {
setState(() {
_gender = value!;
});
}
: null,
),
Text('Laki-laki'),
Radio<String>(
value: 'Perempuan',
groupValue: _gender,
onChanged: _isEditing
? (value) {
setState(() {
_gender = value!;
});
}
: null,
),
Text('Perempuan'),
],
),
SizedBox(height: 20),
Container(
width: double.infinity,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: secondPrimaryColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, 4))],
),
child: Text(
'Detail Kontak',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: whiteColor),
),
),
SizedBox(height: 16),
_buildPhoneField(_phoneController, 'No WA Wali', _isEditing),
_buildEmailField(_emailController, 'Email', _isEditing),
SizedBox(height: 20),
ElevatedButton(
onPressed: _isEditing
? () {
// Trigger the update user function when editing
_updateUserData();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
minimumSize: Size(double.infinity, 40),
),
child: Text(
'Simpan',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
}
Widget _buildTextFormField(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,
enabled: isEnabled,
autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey[200], // Warna latar belakang lebih terang
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40), // Sudut melengkung
borderSide: BorderSide.none, // Menghilangkan border default
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Harap masukkan $labelText Anda';
}
return null;
},
),
SizedBox(height: 20),
],
);
}
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<String>(
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<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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),
],
);
}
}

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
@ -5,9 +7,74 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:ta_tahsin/core/baseurl/base_url.dart';
import 'package:ta_tahsin/core/theme.dart';
class PengajarProfilePage extends StatelessWidget {
class PengajarProfilePage extends StatefulWidget {
const PengajarProfilePage({super.key});
@override
State<PengajarProfilePage> createState() => _PengajarProfilePageState();
}
class _PengajarProfilePageState extends State<PengajarProfilePage> {
String? _name;
String? _email;
String? _age;
String? _phone;
String? _guardianName;
String? _guardianPhone;
String? _address;
String? _gender;
String? _educationLevel;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchUserData();
}
Future<void> _fetchUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString('token');
if (token == null) {
setState(() {
_errorMessage = "Token tidak ditemukan.";
_isLoading = false;
});
return;
}
final response = await http.get(
Uri.parse('${BaseUrl.baseUrl}/user'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_name = data['data']['nama_lengkap'];
_email = data['data']['email'];
_age = data['data']['usia'];
_phone = data['data']['no_telp_wali'];
_guardianName = data['data']['nama_wali'];
_guardianPhone = data['data']['no_telp_wali'];
_address = data['data']['alamat'];
_gender = data['data']['jenis_kelamin'];
_educationLevel = data['data']['jenjang_pendidikan'];
_isLoading = false;
});
} else {
setState(() {
_errorMessage = "Gagal mengambil data pengguna: ${response.body}";
_isLoading = false;
});
}
}
Future<void> logout(BuildContext context) async {
// Ambil token yang ada di SharedPreferences
SharedPreferences prefs = await SharedPreferences.getInstance();
@ -23,22 +90,68 @@ class PengajarProfilePage extends StatelessWidget {
);
if (response.statusCode == 200) {
// Jika berhasil logout, hapus token dari SharedPreferences
prefs.remove('token');
// Arahkan pengguna ke halaman login atau halaman lain setelah logout
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Berhasil logout!')),
);
// If logout is successful, remove the token from SharedPreferences
prefs.remove('token');
// Show success SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white), // Success icon
const SizedBox(width: 10),
Expanded(
child: Text(
'Berhasil logout!',
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.green, // Background color for success
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
),
),
),
);
// Navigate to the login page after logout
// ignore: use_build_context_synchronously
context.go('/');
} else {
// If theres an error from the server, show an error SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white), // Error icon
const SizedBox(width: 10),
Expanded(
child: Text(
'Gagal logout: ${response.body}',
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.red, // Background color for error
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
),
),
),
);
}
// ignore: use_build_context_synchronously
context.go('/login');
} else {
// Jika ada error dari server
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal logout: ${response.body}')),
);
}
} else {
// Jika tidak ada token yang tersimpan
ScaffoldMessenger.of(context).showSnackBar(
@ -52,132 +165,169 @@ class PengajarProfilePage extends StatelessWidget {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(
"Profile",
),
title: Text("Profile"),
automaticallyImplyLeading: false,
),
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Profile Section with Gradient Card
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Profile Avatar
CircleAvatar(
radius: 50,
backgroundColor: Colors.white,
backgroundImage: AssetImage('assets/logo/sho.jpg'),
),
const SizedBox(width: 16),
// Name and Phone Number to the right
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"irfan",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
const SizedBox(height: 5),
Text(
"083166408735",
style: TextStyle(
fontSize: 16, color: Colors.white.withOpacity(0.8)),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [secondPrimaryColor, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.white,
backgroundImage: AssetImage(
'assets/icon/defaultprofile.jpeg'),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
_name ?? "Nama Tidak Ditemukan",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 5),
Text(
_phone ?? "Nomor Telepon Tidak Ditemukan",
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
),
const SizedBox(height: 20),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [secondPrimaryColor, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestResultSection("Tanggal Lahir", _age ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection("Email", _email ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection("Nama Wali",
_guardianName ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Alamat", _address ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Jenis Kelamin", _gender ?? "-"),
Divider(color: Colors.white),
_buildTestResultSection(
"Jenjang Pendidikan", _educationLevel ?? "-"),
],
),
),
),
// "Hasil Placement Test" Section with Gradient Card
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestResultSection("Tgl Lahir", "-"),
Divider(color: Colors.white), // White Divider for contrast
_buildTestResultSection("Alamat", "-"),
Divider(color: Colors.white), // White Divider for contrast
_buildTestResultSection("Nama Orang Tua", "-"),
Divider(color: Colors.white), // White Divider for contrast
_buildTestResultSection("email", "-"),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
),
onPressed: () {
logout(context);
},
child: Text(
"Logout",
style: TextStyle(fontSize: 16,color: whiteColor),
),
),
],
),
),
),
const SizedBox(height: 20),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 5,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
colors: [
secondPrimaryColor,
Colors.blue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoLink("Edit Profile", () {
context.push('/edit_profile_pengajar');
}),
Divider(color: Colors.white),
_buildInfoLink("Ubah Password", () {
context.push('/ubah_password_pengajar');
}),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minimumSize: Size(double.infinity, 50),
),
onPressed: () {
logout(context);
},
child: Text(
"Logout",
style: TextStyle(fontSize: 16, color: whiteColor),
),
),
],
),
),
),
),
);
}
// Method to build test result sections
Widget _buildTestResultSection(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
@ -188,7 +338,7 @@ class PengajarProfilePage extends StatelessWidget {
label,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SizedBox(height: 6), // Space between label and value
SizedBox(height: 6),
Text(
value,
style: TextStyle(fontSize: 16, color: Colors.white),
@ -197,22 +347,25 @@ class PengajarProfilePage extends StatelessWidget {
),
);
}
// Method to create info links
Widget _buildInfoLink(String label) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
Widget _buildInfoLink(String label, VoidCallback onTap) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: GestureDetector(
onTap: onTap, // Menambahkan aksi saat item ditekan
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Mengatur posisi label dan ikon
children: [
Icon(Icons.arrow_forward, color: secondPrimaryColor),
SizedBox(width: 8),
Text(
label,
style: TextStyle(fontSize: 16, color: secondPrimaryColor),
style: TextStyle(fontSize: 16, color: Colors.white),
),
Icon(
Icons.arrow_forward,
color: Colors.white, // Anda bisa menyesuaikan warna ikon sesuai dengan desain Anda
),
],
),
);
}
),
);
}
}

View File

@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
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:http/http.dart' as http;
import 'dart:convert';
class ChangePasswordPengajarPage extends StatefulWidget {
const ChangePasswordPengajarPage({super.key});
@override
_ChangePasswordPengajarPageState createState() => _ChangePasswordPengajarPageState();
}
class _ChangePasswordPengajarPageState extends State<ChangePasswordPengajarPage> {
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
String? _errorMessage;
bool showNewPassword = true;
bool showConfirmPassword = true;
// Update visibility for New Password field
void updateNewPasswordVisibility() {
setState(() {
showNewPassword = !showNewPassword;
});
}
// Update visibility for Confirm Password field
void updateConfirmPasswordVisibility() {
setState(() {
showConfirmPassword = !showConfirmPassword;
});
}
Future<void> handleChangePassword() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('token');
debugPrint("Token yang diambil: $authToken");
// Validasi panjang password minimal 8 karakter
if (_newPasswordController.text.length < 8) {
setState(() {
_errorMessage = 'Password harus minimal 8 karakter';
});
return;
}
// Validasi jika password baru dan konfirmasi password tidak cocok
if (_newPasswordController.text != _confirmPasswordController.text) {
setState(() {
_errorMessage = 'Password baru dan konfirmasi password tidak cocok';
});
return;
}
setState(() {
_errorMessage = null; // Reset error message
});
// URL API untuk mengubah password
String url = '${BaseUrl.baseUrl}/change_password'; // Ganti dengan URL API Anda
debugPrint("URL yang digunakan: $url");
// Data yang akan dikirim
Map<String, dynamic> requestBody = {
'new_password': _newPasswordController.text,
'new_password_confirmation': _confirmPasswordController.text,
};
debugPrint("Request Body: $requestBody");
// Kirim request ke API
final response = await http.post(
Uri.parse(url),
headers: {
'Authorization': 'Bearer $authToken',
'Content-Type': 'application/json', // Pastikan header ini ada
},
body: jsonEncode(requestBody),
);
debugPrint("Status code: ${response.statusCode}");
debugPrint("Response body: ${response.body}");
if (response.statusCode == 200) {
// If the password change is successful
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white), // Success icon
const SizedBox(width: 10),
Expanded(
child: Text(
'Password berhasil diubah',
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.green, // Background color for success
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
),
),
),
);
context.pop();
} else {
final responseBody = json.decode(response.body);
setState(() {
// Displaying error message if available
_errorMessage = responseBody['message']['new_password']?.first ?? 'Terjadi kesalahan, coba lagi';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white), // Failure icon
const SizedBox(width: 10),
Expanded(
child: Text(
'${json.decode(response.body)['data']['message']}',
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.red, // Background color for failure
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
),
),
),
);
}
}
@override
Widget build(BuildContext context) {
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(
"Ubah Password",
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: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 50.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Password Input Field
Text(
'Password Baru',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
TextField(
controller: _newPasswordController,
obscureText: showNewPassword,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
showNewPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateNewPasswordVisibility,
),
filled: true,
fillColor: Colors.grey[200], // Lighter background for the field
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide.none, // Remove default border
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
),
const SizedBox(height: 16),
// Confirm Password Input Field
Text(
'Konfirmasi Password',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
TextField(
controller: _confirmPasswordController,
obscureText: showConfirmPassword,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
showConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: updateConfirmPasswordVisibility,
),
filled: true,
fillColor: Colors.grey[200], // Lighter background for the field
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide.none, // Remove default border
),
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
),
),
const SizedBox(height: 16),
// Error Message
if (_errorMessage != null)
Text(
_errorMessage!,
style: TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 40),
// Send Button
ElevatedButton(
onPressed: () {
handleChangePassword();
},
style: ElevatedButton.styleFrom(
backgroundColor: secondPrimaryColor,
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 15,
),
minimumSize: Size(double.infinity, 40),
),
child: Text(
'Selesai',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}

View File

@ -345,7 +345,7 @@ packages:
source: hosted
version: "1.4.0"
http_parser:
dependency: transitive
dependency: "direct main"
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"

View File

@ -49,6 +49,7 @@ dependencies:
file_picker: ^10.1.9
excel: ^4.0.6
record: ^6.0.0
http_parser: ^4.1.2
dev_dependencies:
flutter_test:
@ -77,6 +78,7 @@ flutter:
- assets/logo/
- assets/audio/
- assets/icon/
- assets/images/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images