23/06/25 fitur validasi pelafalan santri belum
This commit is contained in:
parent
e0b5c274b3
commit
4d896d6628
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
// lib/config.dart
|
// lib/config.dart
|
||||||
class BaseUrl {
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class _NavigationPengajarPageState extends State<NavigationPengajarPage> {
|
||||||
final List<Widget> _pages = [
|
final List<Widget> _pages = [
|
||||||
const KemajuanPage(),
|
const KemajuanPage(),
|
||||||
const DataSantriPage(),
|
const DataSantriPage(),
|
||||||
const DataLatihanPage(),
|
// const DataLatihanPage(),
|
||||||
const PengajarProfilePage(),
|
const PengajarProfilePage(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -52,10 +52,10 @@ class _NavigationPengajarPageState extends State<NavigationPengajarPage> {
|
||||||
icon: Icon(Icons.list_alt),
|
icon: Icon(Icons.list_alt),
|
||||||
label: "Data Santri",
|
label: "Data Santri",
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
// BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.list_alt),
|
// icon: Icon(Icons.list_alt),
|
||||||
label: "Data Latihan",
|
// label: "Data Latihan",
|
||||||
),
|
// ),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.person),
|
icon: Icon(Icons.person),
|
||||||
label: "Profile",
|
label: "Profile",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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/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/detail_quiz.dart';
|
||||||
import 'package:ta_tahsin/view/home/quiz/hasil_quiz.dart';
|
import 'package:ta_tahsin/view/home/quiz/hasil_quiz.dart';
|
||||||
import 'package:ta_tahsin/view/pengajar/data_latihan/detail_data_latihan.dart';
|
import 'package:ta_tahsin/view/pengajar/data_latihan/detail_data_latihan.dart';
|
||||||
import 'package:ta_tahsin/view/pengajar/data_santri/detail_data_santri.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/data_santri/tambah_santri.dart';
|
||||||
import 'package:ta_tahsin/view/pengajar/kemajuan/detail_kemajuan.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/auth/login/login.dart';
|
||||||
import '../../view/home/latihan/pelafalan_popup.dart';
|
import '../../view/home/latihan/pelafalan_popup.dart';
|
||||||
|
|
@ -19,10 +24,10 @@ import '../navigation/navigation.dart';
|
||||||
import '../navigation/navigation_pengajar.dart';
|
import '../navigation/navigation_pengajar.dart';
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: '/login',
|
// initialLocation: '/login',
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/login',
|
path: '/',
|
||||||
builder: (context, state) => const LoginPage(),
|
builder: (context, state) => const LoginPage(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
@ -94,14 +99,22 @@ GoRoute(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/detail_kemajuan',
|
path: '/detail_kemajuan',
|
||||||
builder: (BuildContext context, GoRouterState state) {
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
|
final Map<String, dynamic> extra = state.extra as Map<String, dynamic>;
|
||||||
return DetailKemajuanPage(
|
|
||||||
nama: extra['nama'],
|
// 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(
|
GoRoute(
|
||||||
path: '/data_santri',
|
path: '/data_santri',
|
||||||
builder: (context, state) {
|
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(
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:ta_tahsin/core/baseurl/base_url.dart';
|
import 'package:ta_tahsin/core/baseurl/base_url.dart';
|
||||||
|
|
@ -21,6 +19,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
TextEditingController emailController = TextEditingController();
|
TextEditingController emailController = TextEditingController();
|
||||||
TextEditingController passwordController = TextEditingController();
|
TextEditingController passwordController = TextEditingController();
|
||||||
bool showPass = true;
|
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() {
|
void updateObsecure() {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -29,152 +30,214 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> login() async {
|
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(
|
final response = await http.post(
|
||||||
Uri.parse('${BaseUrl.baseUrl}/login'),
|
Uri.parse('${BaseUrl.baseUrl}/loginWithTelp'),
|
||||||
body: {
|
body: {
|
||||||
'email': emailController.text,
|
'no_telp_wali': emailController.text,
|
||||||
'password': passwordController.text,
|
'password': passwordController.text,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isLoading = false; // Menandakan bahwa login telah selesai
|
||||||
|
});
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var data = json.decode(response.body);
|
var data = json.decode(response.body);
|
||||||
|
|
||||||
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
prefs.setString('token', data['data']['access_token']);
|
prefs.setString('token', data['data']['access_token']);
|
||||||
|
|
||||||
|
|
||||||
String peran = data['data']['user']['peran'];
|
String peran = data['data']['user']['peran'];
|
||||||
prefs.setString('peran', peran);
|
prefs.setString('peran', peran);
|
||||||
|
|
||||||
|
|
||||||
if (peran == 'santri') {
|
if (peran == 'santri') {
|
||||||
router.push("/navigasi");
|
router.push("/navigasi");
|
||||||
} else if (peran == 'pengajar') {
|
} else if (peran == 'pengajar') {
|
||||||
router.push("/navigasiPengajar");
|
router.push("/navigasiPengajar");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// debugPrint("anjing");
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Column(
|
||||||
child: SingleChildScrollView(
|
children: [
|
||||||
child: Padding(
|
// Top half: Background image
|
||||||
padding: PaddingCustom().paddingHorizontal(20),
|
Container(
|
||||||
child: Column(
|
height: MediaQuery.of(context).size.height * 0.5, // Half the screen height
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: double.infinity,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
decoration: BoxDecoration(
|
||||||
children: [
|
image: DecorationImage(
|
||||||
const SizedBox(height: 40),
|
image: AssetImage('assets/images/muhajirin4.jpg'), // Your image
|
||||||
Text(
|
fit: BoxFit.cover,
|
||||||
"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),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
// 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:record/record.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/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.dart';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
class LatihanPage extends StatefulWidget {
|
class LatihanPage extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
|
|
@ -101,14 +103,134 @@ class _LatihanPageState extends State<LatihanPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi untuk menghentikan perekaman
|
// // Fungsi untuk menghentikan perekaman
|
||||||
Future<void> stopRecording() async {
|
// Future<void> stopRecording() async {
|
||||||
await _record.stop();
|
// 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(() {
|
setState(() {
|
||||||
isRecording = false;
|
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() {
|
void stopTimer() {
|
||||||
countdownTimer.cancel();
|
countdownTimer.cancel();
|
||||||
|
|
@ -340,7 +462,10 @@ class _LatihanPageState extends State<LatihanPage> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_record.dispose();
|
_record.dispose();
|
||||||
_audioPlayer.dispose();
|
_audioPlayer.dispose();
|
||||||
|
if (countdownTimer.isActive) {
|
||||||
|
countdownTimer.cancel(); // Cancel the timer
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:ta_tahsin/core/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.dart';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
|
|
||||||
class PelafalanPage extends StatefulWidget {
|
class PelafalanPage extends StatefulWidget {
|
||||||
|
|
@ -65,6 +70,101 @@ class _PelafalanPageState extends State<PelafalanPage> {
|
||||||
print("Audio playing...");
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -163,7 +263,7 @@ class _PelafalanPageState extends State<PelafalanPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Pelafalan Ustadz',
|
'Pelafalan Benar',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: secondPrimaryColor,
|
color: secondPrimaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -231,7 +331,9 @@ class _PelafalanPageState extends State<PelafalanPage> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.go('/navigasi');
|
// updateProgress(widget.id);
|
||||||
|
|
||||||
|
updateProgress(widget.id); // Pass submateri_id (widget.id) and latihan_ids
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:go_router/go_router.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/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.dart';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
|
|
||||||
|
|
@ -23,52 +24,70 @@ class MateriPage extends StatefulWidget {
|
||||||
|
|
||||||
class _MateriPageState extends State<MateriPage> {
|
class _MateriPageState extends State<MateriPage> {
|
||||||
late Future<List<dynamic>> kategoriData;
|
late Future<List<dynamic>> kategoriData;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
kategoriData = fetchKategoriData(widget.id);
|
kategoriData = fetchKategoriData(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<List<dynamic>> fetchKategoriData(int id_materi) async {
|
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) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
return json.decode(response.body)['data'];
|
return json.decode(response.body)['data'];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load kategori');
|
throw Exception('Failed to load kategori');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<List<dynamic>> fetchSubMateriData(int id_kategori) async {
|
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) {
|
if (response.statusCode == 200) {
|
||||||
return json.decode(response.body)['data']['sub_materi'];
|
return json.decode(response.body)['data']['sub_materi'];
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Failed to load sub-materi');
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: Size.fromHeight(50),
|
preferredSize: Size.fromHeight(50),
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
borderRadius: BorderRadius.zero,
|
margin: EdgeInsets.zero,
|
||||||
),
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
backgroundColor: secondPrimaryColor,
|
backgroundColor: secondPrimaryColor,
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -87,26 +106,26 @@ class _MateriPageState extends State<MateriPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: isLoading
|
body:
|
||||||
? Center(child: CircularProgressIndicator())
|
isLoading
|
||||||
: FutureBuilder<List<dynamic>>(
|
? Center(child: CircularProgressIndicator())
|
||||||
future: kategoriData,
|
: FutureBuilder<List<dynamic>>(
|
||||||
builder: (context, snapshot) {
|
future: kategoriData,
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
builder: (context, snapshot) {
|
||||||
return Center(child: CircularProgressIndicator());
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
} else if (snapshot.hasError) {
|
return Center(child: CircularProgressIndicator());
|
||||||
return Center(child: Text('Error: ${snapshot.error}'));
|
} else if (snapshot.hasError) {
|
||||||
} else if (!snapshot.hasData || snapshot.data == null) {
|
return Center(child: Text('Error: ${snapshot.error}'));
|
||||||
return Center(child: Text('Tidak ada data tersedia'));
|
} 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(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate([
|
||||||
[
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
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!;
|
for (var kategori in kategoriList)
|
||||||
|
FutureBuilder<List<dynamic>>(
|
||||||
return SliverList(
|
future: fetchSubMateriData(kategori['id']),
|
||||||
delegate: SliverChildListDelegate(
|
builder: (context, subMateriSnapshot) {
|
||||||
[
|
if (subMateriSnapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
Padding(
|
return SliverToBoxAdapter(child: SizedBox());
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
|
} else if (subMateriSnapshot.hasError) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
child: Text(
|
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(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -156,21 +187,26 @@ class _MateriPageState extends State<MateriPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
for (var submateri in subMateriList)
|
for (var submateri in subMateriList)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 0),
|
padding: const EdgeInsets.only(bottom: 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
horizontal: 12.0,
|
||||||
|
),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: secondPrimaryColor,
|
color: secondPrimaryColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius:
|
||||||
|
BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.menu_book,
|
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(
|
subtitle: Column(
|
||||||
submateri['subtitle'],
|
crossAxisAlignment:
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
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: () {
|
onTap: () {
|
||||||
context.push(
|
context.push(
|
||||||
'/submateri',
|
'/submateri',
|
||||||
extra: {
|
extra: {
|
||||||
'id': submateri['id'],
|
'id': submateri['id'],
|
||||||
'title': submateri['title'],
|
'title': submateri['title'],
|
||||||
'description': submateri['subtitle'],
|
'description':
|
||||||
'videoLink': submateri['video_url'],
|
submateri['subtitle'],
|
||||||
|
'videoLink':
|
||||||
|
submateri['video_url'],
|
||||||
'intro': submateri['intro'],
|
'intro': submateri['intro'],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -217,15 +441,14 @@ class _MateriPageState extends State<MateriPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
]),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,45 +2,103 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
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/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.dart';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatelessWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
const ProfilePage({super.key});
|
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 {
|
Future<void> logout(BuildContext context) async {
|
||||||
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
String? token = prefs.getString('token');
|
String? token = prefs.getString('token');
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse('${BaseUrl.baseUrl}/logout'),
|
Uri.parse('${BaseUrl.baseUrl}/logout'),
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
||||||
prefs.remove('token');
|
prefs.remove('token');
|
||||||
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Berhasil logout!')),
|
SnackBar(content: Text('Berhasil logout!')),
|
||||||
);
|
);
|
||||||
|
context.go('/');
|
||||||
|
|
||||||
context.go('/login');
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Gagal logout: ${response.body}')),
|
SnackBar(content: Text('Gagal logout: ${response.body}')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Token tidak ditemukan')),
|
SnackBar(content: Text('Token tidak ditemukan')),
|
||||||
);
|
);
|
||||||
|
|
@ -52,132 +110,173 @@ class ProfilePage extends StatelessWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text("Profile"),
|
||||||
"Profile",
|
|
||||||
),
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: _isLoading
|
||||||
child: SingleChildScrollView(
|
? Center(child: CircularProgressIndicator())
|
||||||
child: Padding(
|
: _errorMessage != null
|
||||||
padding: const EdgeInsets.all(16),
|
? Center(child: Text(_errorMessage!))
|
||||||
child: Column(
|
: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: SingleChildScrollView(
|
||||||
children: [
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
Card(
|
child: Column(
|
||||||
shape: RoundedRectangleBorder(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
borderRadius: BorderRadius.circular(15),
|
children: [
|
||||||
),
|
Card(
|
||||||
elevation: 5,
|
shape: RoundedRectangleBorder(
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(15),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
elevation: 5,
|
||||||
Text(
|
child: Container(
|
||||||
"083166408735",
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(15),
|
||||||
fontSize: 16, color: Colors.white.withOpacity(0.8)),
|
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),
|
||||||
],
|
Card(
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
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 ?? "-"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
Card(
|
Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
secondPrimaryColor,
|
secondPrimaryColor,
|
||||||
Colors.blue,
|
Colors.blue,
|
||||||
],
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
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) {
|
Widget _buildTestResultSection(String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||||
|
|
@ -188,7 +287,7 @@ class ProfilePage extends StatelessWidget {
|
||||||
label,
|
label,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||||
),
|
),
|
||||||
SizedBox(height: 6),
|
SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
|
@ -197,22 +296,25 @@ class ProfilePage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Widget _buildInfoLink(String label, VoidCallback onTap) {
|
||||||
|
return Padding(
|
||||||
Widget _buildInfoLink(String label) {
|
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||||
return Padding(
|
child: GestureDetector(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
onTap: onTap, // Menambahkan aksi saat item ditekan
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Mengatur posisi label dan ikon
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.arrow_forward, color: secondPrimaryColor),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
Text(
|
||||||
label,
|
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
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -277,7 +277,7 @@ class _DataSantriPageState extends State<DataSantriPage> {
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/tambah_santri');
|
context.push('/tambah_santri');
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
|
|
@ -400,7 +400,7 @@ class _DataSantriPageState extends State<DataSantriPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.go('/detail_user', extra: {
|
context.push('/detail_user', extra: {
|
||||||
'id': santri['id'],
|
'id': santri['id'],
|
||||||
});
|
});
|
||||||
// router.push("/detail_user");
|
// router.push("/detail_user");
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
|
||||||
final TextEditingController _jenjangPendidikanController = TextEditingController();
|
final TextEditingController _jenjangPendidikanController = TextEditingController();
|
||||||
String _gender = 'Laki-laki';
|
String _gender = 'Laki-laki';
|
||||||
bool _isEditing = false; // Track whether we are in edit mode
|
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
|
@override
|
||||||
void initState() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -149,7 +169,8 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.arrow_back),
|
icon: Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/navigasiPengajar');
|
// context.go('/navigasiPengajar');
|
||||||
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -192,9 +213,9 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
_buildTextFormField(_fullNameController, 'Nama Lengkap', _isEditing),
|
_buildTextFormField(_fullNameController, 'Nama Lengkap', _isEditing),
|
||||||
_buildTextFormField(_addressController, 'Alamat', _isEditing),
|
_buildAddressField(_addressController, 'Alamat', _isEditing),
|
||||||
_buildTextFormField(_dobController, 'Usia', _isEditing),
|
_buildDateOfBirthField(_dobController, 'Tanggal Lahir', _isEditing),
|
||||||
_buildTextFormField(_jenjangPendidikanController, 'Jenjang Pendidikan', _isEditing),
|
_buildDropdownJenjangPendidikan( 'Jenjang Pendidikan', _isEditing),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
|
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
|
||||||
|
|
@ -239,32 +260,33 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
_buildTextFormField(_phoneController, 'No WA Wali', _isEditing),
|
_buildPhoneField(_phoneController, 'No WA Wali', _isEditing),
|
||||||
_buildTextFormField(_emailController, 'Email', _isEditing),
|
_buildEmailField(_emailController, 'Email', _isEditing),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Padding(
|
ElevatedButton(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20),
|
onPressed: _isEditing
|
||||||
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
|
// Trigger the update user function when editing
|
||||||
_updateUserData();
|
_updateUserData();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
style: ElevatedButton.styleFrom(
|
||||||
"Simpan",
|
backgroundColor: secondPrimaryColor,
|
||||||
style: TextStyle(fontSize: 16, color: whiteColor),
|
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) {
|
Widget _buildTextFormField(TextEditingController controller, String labelText, bool isEnabled) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -281,19 +302,16 @@ class _DetailDataSantriPageState extends State<DetailDataSantriPage> {
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: isEnabled, // Control whether the field is enabled or not
|
enabled: isEnabled,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.grey[200], // Warna latar belakang lebih terang
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.grey),
|
borderRadius: BorderRadius.circular(40), // Sudut melengkung
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderSide: BorderSide.none, // Menghilangkan border default
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
borderSide: BorderSide(color: Colors.blue),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,14 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final TextEditingController _fullNameController = TextEditingController();
|
final TextEditingController _fullNameController = TextEditingController();
|
||||||
final TextEditingController _addressController = TextEditingController();
|
final TextEditingController _addressController = TextEditingController();
|
||||||
final TextEditingController _dobController = TextEditingController();
|
final TextEditingController _dobController = TextEditingController(); // For DOB
|
||||||
final TextEditingController _phoneController = TextEditingController();
|
final TextEditingController _phoneController = TextEditingController();
|
||||||
final TextEditingController _emailController = TextEditingController();
|
final TextEditingController _emailController = TextEditingController();
|
||||||
final TextEditingController _jenjangPendidikanController = TextEditingController();
|
|
||||||
String _gender = 'Laki-laki';
|
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 {
|
Future<void> _submitForm() async {
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
|
@ -44,11 +47,11 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
final Map<String, String> data = {
|
final Map<String, String> data = {
|
||||||
'nama_lengkap': _fullNameController.text,
|
'nama_lengkap': _fullNameController.text,
|
||||||
'alamat': _addressController.text,
|
'alamat': _addressController.text,
|
||||||
'usia': _dobController.text,
|
'usia': _dobController.text, // Use formatted DOB
|
||||||
'no_telp_wali': _phoneController.text,
|
'no_telp_wali': _phoneController.text,
|
||||||
'email': _emailController.text,
|
'email': _emailController.text,
|
||||||
'jenis_kelamin': _gender,
|
'jenis_kelamin': _gender,
|
||||||
'jenjang_pendidikan': _jenjangPendidikanController.text,
|
'jenjang_pendidikan': _selectedJenjangPendidikan, // Send selected value
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
|
|
@ -74,7 +77,7 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
context.go('/navigasiPengajar');
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
),
|
),
|
||||||
|
|
@ -91,7 +94,7 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/navigasiPengajar');
|
context.pop();
|
||||||
},
|
},
|
||||||
child: Text('OK'),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: PreferredSize(
|
||||||
title: Text('Tambah Santri'),
|
preferredSize: Size.fromHeight(50),
|
||||||
leading: IconButton(
|
child: Card(
|
||||||
icon: Icon(Icons.arrow_back),
|
elevation: 4,
|
||||||
onPressed: () {
|
shape: RoundedRectangleBorder(
|
||||||
context.go('/navigasiPengajar');
|
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(
|
body: SingleChildScrollView(
|
||||||
|
|
@ -124,9 +162,9 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildTextFormField(_fullNameController, 'Nama Lengkap'),
|
_buildTextFormField(_fullNameController, 'Nama Lengkap'),
|
||||||
_buildTextFormField(_addressController, 'Alamat'),
|
_buildAddressField(_addressController,'Alamat'),
|
||||||
_buildTextFormField(_dobController, 'Usia'),
|
_buildDateOfBirthField(), // Date of Birth Picker
|
||||||
_buildTextFormField(_jenjangPendidikanController, 'Jenjang Pendidikan'),
|
_buildDropdownJenjangPendidikan(), // Jenjang Pendidikan Dropdown
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
|
Text('Jenis Kelamin', style: TextStyle(fontSize: 16)),
|
||||||
|
|
@ -153,30 +191,31 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
_buildTextFormField(_phoneController, 'No WA Wali'),
|
_buildPhoneField(), // No WA Wali input with icon on right
|
||||||
_buildTextFormField(_emailController, 'Email'),
|
_buildEmailField(), // Email input with icon on right
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Padding(
|
ElevatedButton(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20),
|
onPressed: () async {
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: secondPrimaryColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
minimumSize: Size(double.infinity, 50),
|
|
||||||
elevation: 5,
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
print("Simpan Taped");
|
print("Simpan Taped");
|
||||||
await _submitForm();
|
await _submitForm();
|
||||||
},
|
},
|
||||||
child: Text(
|
style: ElevatedButton.styleFrom(
|
||||||
"Simpan",
|
backgroundColor: secondPrimaryColor,
|
||||||
style: TextStyle(fontSize: 16, color: whiteColor),
|
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) {
|
Widget _buildTextFormField(TextEditingController controller, String labelText) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -194,18 +267,15 @@ class _TambahSantriPageState extends State<TambahSantriPage> {
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction, // Validasi otomatis saat input
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.grey[200], // Warna latar belakang lebih terang
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.grey),
|
borderRadius: BorderRadius.circular(40), // Sudut melengkung
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderSide: BorderSide.none, // Menghilangkan border default
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
contentPadding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
borderSide: BorderSide(color: Colors.blue),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,212 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
|
|
||||||
class DetailKemajuanPage extends StatelessWidget {
|
class DetailKemajuanPage extends StatefulWidget {
|
||||||
const DetailKemajuanPage({super.key, required this.nama});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: PreferredSize(
|
||||||
title: const Text('Detail Kemajuan'),
|
preferredSize: Size.fromHeight(60),
|
||||||
leading: IconButton(
|
child: Card(
|
||||||
icon: const Icon(Icons.arrow_back),color: blackColor,
|
elevation: 4,
|
||||||
onPressed: () {
|
shape: RoundedRectangleBorder(
|
||||||
if (context.canPop()) {
|
borderRadius: BorderRadius.zero,
|
||||||
context.pop();
|
),
|
||||||
} else {
|
margin: EdgeInsets.zero,
|
||||||
context.go('/navigasiPengajar');
|
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(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 50.0),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// Foto Profil Pengguna
|
children: [
|
||||||
Center(
|
// Loading Spinner while fetching data
|
||||||
child: CircleAvatar(
|
if (isLoading)
|
||||||
radius: 70,
|
Center(child: CircularProgressIndicator())
|
||||||
backgroundImage: AssetImage('assets/icon/defaultprofile.jpeg'), // Ganti dengan gambar profil
|
else ...[
|
||||||
),
|
// Profile Section with CircleAvatar and Shadow
|
||||||
),
|
Center(
|
||||||
const SizedBox(height: 16),
|
child: CircleAvatar(
|
||||||
// Nama Pengguna
|
radius: 60,
|
||||||
Center(
|
backgroundImage: AssetImage(userProfileImage), // Use dynamic profile image
|
||||||
child: Text(
|
backgroundColor: Colors.transparent,
|
||||||
nama, // Ganti dengan nama pengguna
|
child: Container(
|
||||||
style: const TextStyle(
|
decoration: BoxDecoration(
|
||||||
fontSize: 24,
|
shape: BoxShape.circle,
|
||||||
fontWeight: FontWeight.bold,
|
boxShadow: [
|
||||||
color: Colors.black,
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
),
|
// Name Section
|
||||||
const SizedBox(height: 40),
|
Center(
|
||||||
// Judul Progres Belajar
|
child: Text(
|
||||||
Text(
|
widget.nama, // Use dynamic user name
|
||||||
'Progres Belajar',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 20,
|
||||||
fontSize: 22,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
color: blackColor,
|
||||||
color: blackColor,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 24),
|
||||||
// Progress Bar untuk materi
|
// Progres Belajar Title
|
||||||
ProgressBar(title: 'Jumlah Materi 1 Selesai', progress: 0.6), // Progress 60%
|
Text(
|
||||||
const SizedBox(height: 20),
|
'Progres Belajar',
|
||||||
ProgressBar(title: 'Jumlah Materi 2 Selesai', progress: 0.4), // Progress 40%
|
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,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: blackColor,
|
color: blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 10),
|
||||||
LinearProgressIndicator(
|
Stack(
|
||||||
value: progress,
|
alignment: Alignment.center,
|
||||||
backgroundColor: Colors.grey[300],
|
children: [
|
||||||
color: secondPrimaryColor, // Ganti dengan warna yang sesuai
|
// Linear Progress Bar with rounded corners
|
||||||
minHeight: 20,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,167 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
// Pastikan untuk menyesuaikan import theme.dart sesuai lokasi file theme.dart Anda
|
import 'package:ta_tahsin/core/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -22,6 +176,12 @@ class KemajuanPage extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
// Search bar
|
// Search bar
|
||||||
TextField(
|
TextField(
|
||||||
|
onChanged: (query) {
|
||||||
|
setState(() {
|
||||||
|
searchQuery = query;
|
||||||
|
filterKemajuan(searchQuery);
|
||||||
|
});
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
hintText: 'Cari Santri...',
|
hintText: 'Cari Santri...',
|
||||||
|
|
@ -40,102 +200,108 @@ class KemajuanPage extends StatelessWidget {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color:
|
color: secondPrimaryColor,
|
||||||
secondPrimaryColor, // Gunakan secondPrimaryColor dari theme.dart
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Menggunakan SliverList dengan kemajuanList
|
// Jika loading, tampilkan loading indicator
|
||||||
Expanded(
|
if (isLoading)
|
||||||
child: CustomScrollView(
|
Center(child: CircularProgressIndicator())
|
||||||
slivers: [
|
else if (filteredKemajuanList.isEmpty)
|
||||||
SliverList(
|
Center(child: Text('Tidak ada progres latihan yang ditemukan.'))
|
||||||
delegate: SliverChildBuilderDelegate(
|
else
|
||||||
(context, index) {
|
// Menggunakan SliverList dengan kemajuanList yang sudah difilter
|
||||||
// Mengambil data kemajuan dari kemajuanList
|
Expanded(
|
||||||
var kemajuan = kemajuanList[index];
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
var kemajuan = filteredKemajuanList[index];
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Menampilkan detail subMateri, menggunakan subMateri yang terkait
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(bottom: 0),
|
||||||
padding: const EdgeInsets.only(bottom: 0),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
ListTile(
|
||||||
ListTile(
|
contentPadding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
contentPadding:
|
leading: Container(
|
||||||
const EdgeInsets.symmetric(
|
width: 60,
|
||||||
vertical: 5.0,
|
height: 60,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
leading: Container(
|
shape: BoxShape.circle,
|
||||||
width: 60,
|
image: DecorationImage(
|
||||||
height: 60,
|
image: AssetImage(
|
||||||
decoration: BoxDecoration(
|
'assets/icon/defaultprofile.jpeg',
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
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: () {
|
Divider(
|
||||||
context.go('/detail_kemajuan',
|
color: Colors.grey.withOpacity(0.5),
|
||||||
extra: {'nama': kemajuan['nama']},
|
thickness: 1,
|
||||||
);
|
indent: 40,
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
Divider(
|
),
|
||||||
color: Colors.grey.withOpacity(0.5),
|
|
||||||
thickness: 1,
|
|
||||||
indent: 80,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
childCount: filteredKemajuanList.length,
|
||||||
childCount:
|
),
|
||||||
kemajuanList
|
|
||||||
.length, // Menyesuaikan jumlah item yang ada pada kemajuanList
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart' as http;
|
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/baseurl/base_url.dart';
|
||||||
import 'package:ta_tahsin/core/theme.dart';
|
import 'package:ta_tahsin/core/theme.dart';
|
||||||
|
|
||||||
class PengajarProfilePage extends StatelessWidget {
|
class PengajarProfilePage extends StatefulWidget {
|
||||||
const PengajarProfilePage({super.key});
|
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 {
|
Future<void> logout(BuildContext context) async {
|
||||||
// Ambil token yang ada di SharedPreferences
|
// Ambil token yang ada di SharedPreferences
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
|
@ -23,22 +90,68 @@ class PengajarProfilePage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
// Jika berhasil logout, hapus token dari SharedPreferences
|
// If logout is successful, remove the token from SharedPreferences
|
||||||
prefs.remove('token');
|
prefs.remove('token');
|
||||||
|
|
||||||
// Arahkan pengguna ke halaman login atau halaman lain setelah logout
|
// Show success SnackBar
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Berhasil logout!')),
|
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 there’s 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 {
|
} else {
|
||||||
// Jika tidak ada token yang tersimpan
|
// Jika tidak ada token yang tersimpan
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -52,132 +165,169 @@ class PengajarProfilePage extends StatelessWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text("Profile"),
|
||||||
"Profile",
|
|
||||||
),
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: _isLoading
|
||||||
child: SingleChildScrollView(
|
? Center(child: CircularProgressIndicator())
|
||||||
child: Padding(
|
: _errorMessage != null
|
||||||
padding: const EdgeInsets.all(16),
|
? Center(child: Text(_errorMessage!))
|
||||||
child: Column(
|
: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: SingleChildScrollView(
|
||||||
children: [
|
child: Padding(
|
||||||
// Profile Section with Gradient Card
|
padding: const EdgeInsets.all(16),
|
||||||
Card(
|
child: Column(
|
||||||
shape: RoundedRectangleBorder(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
borderRadius: BorderRadius.circular(15),
|
children: [
|
||||||
),
|
Card(
|
||||||
elevation: 5,
|
shape: RoundedRectangleBorder(
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(15),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
elevation: 5,
|
||||||
Text(
|
child: Container(
|
||||||
"083166408735",
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(15),
|
||||||
fontSize: 16, color: Colors.white.withOpacity(0.8)),
|
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),
|
||||||
],
|
Card(
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
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
|
const SizedBox(height: 20),
|
||||||
Card(
|
Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
secondPrimaryColor,
|
secondPrimaryColor,
|
||||||
Colors.blue,
|
Colors.blue,
|
||||||
],
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
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) {
|
Widget _buildTestResultSection(String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||||
|
|
@ -188,7 +338,7 @@ class PengajarProfilePage extends StatelessWidget {
|
||||||
label,
|
label,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||||
),
|
),
|
||||||
SizedBox(height: 6), // Space between label and value
|
SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
|
@ -197,22 +347,25 @@ class PengajarProfilePage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Widget _buildInfoLink(String label, VoidCallback onTap) {
|
||||||
// Method to create info links
|
return Padding(
|
||||||
Widget _buildInfoLink(String label) {
|
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||||
return Padding(
|
child: GestureDetector(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
onTap: onTap, // Menambahkan aksi saat item ditekan
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Mengatur posisi label dan ikon
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.arrow_forward, color: secondPrimaryColor),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
Text(
|
||||||
label,
|
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
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -345,7 +345,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ dependencies:
|
||||||
file_picker: ^10.1.9
|
file_picker: ^10.1.9
|
||||||
excel: ^4.0.6
|
excel: ^4.0.6
|
||||||
record: ^6.0.0
|
record: ^6.0.0
|
||||||
|
http_parser: ^4.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
@ -77,6 +78,7 @@ flutter:
|
||||||
- assets/logo/
|
- assets/logo/
|
||||||
- assets/audio/
|
- assets/audio/
|
||||||
- assets/icon/
|
- assets/icon/
|
||||||
|
- assets/images/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue