717 lines
28 KiB
Dart
717 lines
28 KiB
Dart
// lib/features/home/tabs/settings_tab.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart'; // Penting untuk FilteringTextInputFormatter
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:smpn1_lengkong/core/constants/app_colors.dart';
|
|
import 'package:smpn1_lengkong/core/models/student_model.dart';
|
|
import 'package:smpn1_lengkong/core/routes/app_routes.dart';
|
|
import 'package:smpn1_lengkong/core/service/auth_service.dart';
|
|
import 'package:smpn1_lengkong/core/service/student_service.dart';
|
|
|
|
class SettingsTab extends StatelessWidget {
|
|
final StudentModel student;
|
|
final VoidCallback onRefresh;
|
|
|
|
const SettingsTab({
|
|
super.key,
|
|
required this.student,
|
|
required this.onRefresh,
|
|
});
|
|
|
|
// Helper untuk membersihkan nilai "-" agar menjadi string kosong di Form
|
|
String _cleanValue(String? val) {
|
|
if (val == null || val.trim() == "-") return "";
|
|
return val;
|
|
}
|
|
|
|
// --- HELPER DIALOG SUKSES ---
|
|
void _showSuccessDialog(BuildContext context, String message) {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.check_circle_outline_rounded,
|
|
color: AppColors.successGreen,
|
|
size: 64.sp,
|
|
),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
"Berhasil!",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18.sp,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(message, textAlign: TextAlign.center),
|
|
],
|
|
),
|
|
actions: [
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text("Tutup"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- HELPER DIALOG PERINGATAN (TIDAK ADA PERUBAHAN) ---
|
|
void _showNoChangesDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
),
|
|
title: const Text(
|
|
"Tidak Ada Perubahan",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: const Text(
|
|
"Tidak ada perubahan data yang dilakukan. Silakan ubah data terlebih dahulu sebelum menyimpan.",
|
|
),
|
|
actions: [
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text("Tutup"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColors.backgroundWhite,
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
expandedHeight: 120.h,
|
|
pinned: true,
|
|
automaticallyImplyLeading: false,
|
|
backgroundColor: AppColors.primaryBlue,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
titlePadding: EdgeInsets.only(left: 20.w, bottom: 16.h),
|
|
title: Text(
|
|
"Pengaturan",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
background: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [AppColors.primaryBlue, Color(0xFF2C64B8)],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 20.h),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionHeader("PENGATURAN & DUKUNGAN"),
|
|
_buildSettingsCard([
|
|
_buildSettingsTile(
|
|
icon: Icons.person_outline_rounded,
|
|
title: "Informasi Pribadi",
|
|
subtitle: "Detail data orang tua dan alamat",
|
|
onTap: () => _showPersonalInfo(context),
|
|
),
|
|
_buildSettingsTile(
|
|
icon: Icons.phone_rounded,
|
|
title: "Pusat Bantuan",
|
|
subtitle: "Hubungi kami melalui WhatsApp",
|
|
onTap: () async {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder:
|
|
(context) => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
|
|
try {
|
|
final String? contactUrl =
|
|
await StudentService().getContactLink();
|
|
|
|
if (context.mounted) Navigator.pop(context);
|
|
|
|
if (contactUrl != null && contactUrl.isNotEmpty) {
|
|
await StudentService().launchWhatsApp(contactUrl);
|
|
} else {
|
|
throw "Kontak tidak ditemukan di server";
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
if (Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(e.toString())),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
]),
|
|
SizedBox(height: 120.h),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _showLogoutDialog(context),
|
|
icon: const Icon(
|
|
Icons.logout_rounded,
|
|
color: AppColors.primaryBlue,
|
|
),
|
|
label: const Text("Keluar Aplikasi"),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: AppColors.primaryBlue,
|
|
minimumSize: Size(double.infinity, 52.h),
|
|
side: const BorderSide(
|
|
color: AppColors.primaryBlue,
|
|
width: 1.5,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
Center(
|
|
child: Text(
|
|
"Versi 1.0.0",
|
|
style: TextStyle(color: Colors.grey, fontSize: 12.sp),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showPersonalInfo(BuildContext context) {
|
|
bool isEditMode = false;
|
|
bool isSaving = false;
|
|
final formKey = GlobalKey<FormState>();
|
|
|
|
// Ambil data asli yang sudah dibersihkan
|
|
final origFather = _cleanValue(student.parentInfo.fatherName);
|
|
final origMother = _cleanValue(student.parentInfo.motherName);
|
|
final origJob = _cleanValue(student.parentInfo.job);
|
|
final origPhone = _cleanValue(student.parentInfo.phoneNumber);
|
|
final origAddress = _cleanValue(student.parentInfo.address);
|
|
|
|
// Controller menggunakan data asli yang bersih
|
|
final fatherCtrl = TextEditingController(text: origFather);
|
|
final motherCtrl = TextEditingController(text: origMother);
|
|
final jobCtrl = TextEditingController(text: origJob);
|
|
final phoneCtrl = TextEditingController(text: origPhone);
|
|
final addressCtrl = TextEditingController(text: origAddress);
|
|
|
|
showModalBottomSheet(
|
|
context: context, // Menggunakan context utama dari Scaffold
|
|
isScrollControlled: true,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
|
|
),
|
|
builder:
|
|
(sheetContext) => StatefulBuilder(
|
|
builder:
|
|
(modalStateContext, setState) => Container(
|
|
padding: EdgeInsets.fromLTRB(
|
|
24.w,
|
|
12.h,
|
|
24.w,
|
|
MediaQuery.of(modalStateContext).viewInsets.bottom + 32.h,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40.w,
|
|
height: 4.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 24.h),
|
|
Text(
|
|
isEditMode
|
|
? "Ubah Informasi Pribadi"
|
|
: "Detail Informasi Pribadi",
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primaryBlue,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
if (!isEditMode) ...[
|
|
_buildInfoDisplay(
|
|
"Nama Ayah",
|
|
student.parentInfo.fatherName,
|
|
),
|
|
_buildInfoDisplay(
|
|
"Nama Ibu",
|
|
student.parentInfo.motherName,
|
|
),
|
|
_buildInfoDisplay(
|
|
"Pekerjaan Orang Tua",
|
|
student.parentInfo.job,
|
|
),
|
|
_buildInfoDisplay(
|
|
"No. Telepon",
|
|
student.parentInfo.phoneNumber,
|
|
),
|
|
_buildInfoDisplay(
|
|
"Alamat Lengkap",
|
|
student.parentInfo.address,
|
|
),
|
|
SizedBox(height: 24.h),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed:
|
|
() => setState(() => isEditMode = true),
|
|
icon: const Icon(Icons.edit_note_rounded),
|
|
label: const Text("Ubah Informasi Pribadi"),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: 14.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
] else ...[
|
|
Form(
|
|
key: formKey,
|
|
child: Column(
|
|
children: [
|
|
_buildEditField(
|
|
"Nama Ayah",
|
|
fatherCtrl,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(
|
|
RegExp(r'[a-zA-Z\s]'),
|
|
),
|
|
],
|
|
),
|
|
_buildEditField(
|
|
"Nama Ibu",
|
|
motherCtrl,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(
|
|
RegExp(r'[a-zA-Z\s]'),
|
|
),
|
|
],
|
|
),
|
|
_buildEditField(
|
|
"Pekerjaan",
|
|
jobCtrl,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(
|
|
RegExp(r'[a-zA-Z\s]'),
|
|
),
|
|
],
|
|
),
|
|
_buildEditField(
|
|
"No. Telepon",
|
|
phoneCtrl,
|
|
keyboardType: TextInputType.phone,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
],
|
|
),
|
|
_buildEditField(
|
|
"Alamat",
|
|
addressCtrl,
|
|
maxLines: 2,
|
|
),
|
|
SizedBox(height: 24.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed:
|
|
() => setState(
|
|
() => isEditMode = false,
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 14.h,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(
|
|
12.r,
|
|
),
|
|
),
|
|
),
|
|
child: const Text("Batal"),
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed:
|
|
isSaving
|
|
? null
|
|
: () {
|
|
if (formKey.currentState!
|
|
.validate()) {
|
|
final currFather =
|
|
fatherCtrl.text.trim();
|
|
final currMother =
|
|
motherCtrl.text.trim();
|
|
final currJob =
|
|
jobCtrl.text.trim();
|
|
final currPhone =
|
|
phoneCtrl.text.trim();
|
|
final currAddress =
|
|
addressCtrl.text.trim();
|
|
|
|
// Cek apakah ada perubahan data
|
|
final bool hasChanges =
|
|
currFather !=
|
|
origFather ||
|
|
currMother !=
|
|
origMother ||
|
|
currJob != origJob ||
|
|
currPhone !=
|
|
origPhone ||
|
|
currAddress !=
|
|
origAddress;
|
|
|
|
if (!hasChanges) {
|
|
// Menampilkan pop up alert jika tidak ada perubahan
|
|
_showNoChangesDialog(
|
|
context,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Lanjut ke dialog konfirmasi jika data berubah
|
|
_showConfirmUpdateDialog(
|
|
context,
|
|
setState,
|
|
fatherCtrl,
|
|
motherCtrl,
|
|
jobCtrl,
|
|
addressCtrl,
|
|
phoneCtrl,
|
|
(val) => isSaving = val,
|
|
);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 14.h,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(
|
|
12.r,
|
|
),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child:
|
|
isSaving
|
|
? SizedBox(
|
|
height: 20.w,
|
|
width: 20.w,
|
|
child:
|
|
const CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: Text(
|
|
"Simpan",
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showConfirmUpdateDialog(
|
|
BuildContext context,
|
|
StateSetter setState,
|
|
TextEditingController fatherCtrl,
|
|
TextEditingController motherCtrl,
|
|
TextEditingController jobCtrl,
|
|
TextEditingController addressCtrl,
|
|
TextEditingController phoneCtrl,
|
|
Function(bool) setSaving,
|
|
) {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(confirmContext) => AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
),
|
|
title: const Text("Konfirmasi Perubahan"),
|
|
content: const Text(
|
|
"Apakah Anda yakin ingin memperbarui informasi pribadi ini?",
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(confirmContext),
|
|
child: const Text(
|
|
"Batal",
|
|
style: TextStyle(color: Colors.black),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
Navigator.pop(confirmContext); // Tutup dialog konfirmasi
|
|
setState(() => setSaving(true)); // Loading di BottomSheet
|
|
|
|
try {
|
|
await StudentService().updateStudentPersonalData(
|
|
student: student,
|
|
fatherName: fatherCtrl.text.trim(),
|
|
motherName: motherCtrl.text.trim(),
|
|
job: jobCtrl.text.trim(),
|
|
address: addressCtrl.text.trim(),
|
|
phoneNumber: phoneCtrl.text.trim(),
|
|
);
|
|
|
|
if (context.mounted) {
|
|
Navigator.pop(context); // Tutup BottomSheet
|
|
onRefresh(); // Refresh data utama
|
|
_showSuccessDialog(context, "Data berhasil diperbarui.");
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
setState(() => setSaving(false));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Gagal: ${e.toString()}"),
|
|
backgroundColor: AppColors.dangerRed,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text("Simpan"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoDisplay(String label, String value) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: 16.h),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Divider(color: Colors.grey.shade100, height: 1),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEditField(
|
|
String label,
|
|
TextEditingController ctrl, {
|
|
TextInputType? keyboardType,
|
|
int maxLines = 1,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
}) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: 12.h),
|
|
child: TextFormField(
|
|
controller: ctrl,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines,
|
|
inputFormatters: inputFormatters,
|
|
// Disamakan persis dengan _buildInfoDisplay style
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
labelStyle: TextStyle(fontSize: 12.sp, color: Colors.grey),
|
|
isDense: true,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
|
|
),
|
|
validator:
|
|
(v) => (v == null || v.trim().isEmpty) ? "Wajib diisi" : null,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title) => Padding(
|
|
padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 12.h),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColors.textSecondary,
|
|
letterSpacing: 1.2,
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _buildSettingsCard(List<Widget> children) => Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 20.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(children: children),
|
|
);
|
|
|
|
Widget _buildSettingsTile({
|
|
required IconData icon,
|
|
required String title,
|
|
String? subtitle,
|
|
VoidCallback? onTap,
|
|
}) => ListTile(
|
|
onTap: onTap,
|
|
leading: Container(
|
|
padding: EdgeInsets.all(10.w),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primaryBlue.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
child: Icon(icon, color: AppColors.primaryBlue, size: 22.sp),
|
|
),
|
|
title: Text(
|
|
title,
|
|
style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w600),
|
|
),
|
|
subtitle:
|
|
subtitle != null
|
|
? Text(subtitle, style: TextStyle(fontSize: 12.sp))
|
|
: null,
|
|
trailing: Icon(Icons.chevron_right_rounded, color: Colors.grey[400]),
|
|
);
|
|
|
|
void _showLogoutDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
),
|
|
title: const Text("Konfirmasi Keluar"),
|
|
content: const Text("Apakah Anda yakin ingin mengakhiri sesi?"),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("Batal"),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
await AuthService().logout();
|
|
if (context.mounted) {
|
|
Navigator.pushNamedAndRemoveUntil(
|
|
context,
|
|
AppRoutes.login,
|
|
(r) => false,
|
|
);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text("Keluar"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|