MIF_E31231033/lib/screens/dashboard/user/profile_user_screen.dart

1360 lines
49 KiB
Dart

import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../../services/auth_service.dart';
import '../../../routes/app_routes.dart';
class ProfileUserScreen extends StatefulWidget {
const ProfileUserScreen({super.key});
@override
State<ProfileUserScreen> createState() => _ProfileUserScreenState();
}
class _ProfileUserScreenState extends State<ProfileUserScreen> {
bool isLoading = true;
bool isProcessing = false;
Map<String, dynamic> userData = {};
File? _imageFile;
final ImagePicker _picker = ImagePicker();
bool showOldPassword = false;
bool showNewPassword = false;
bool showConfirmPassword = false;
// ── Design tokens ──────────────────────────────────────────────
static const _blue = Color(0xFF3B82F6);
static const _bgPage = Color(0xFFF0F4FA);
static const _cardBg = Colors.white;
static const _textPrimary = Color(0xFF0F172A);
static const _textSecondary = Color(0xFF64748B);
static const _textHint = Color(0xFF94A3B8);
static const _border = Color(0xFFE2E8F0);
@override
void initState() {
super.initState();
loadProfile();
}
Future<void> loadProfile() async {
final result = await AuthService.getProfile();
if (!mounted) return;
if (result['success']) {
setState(() {
userData = result['data'];
isLoading = false;
});
} else {
setState(() => isLoading = false);
}
}
Future<void> pickImage() async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
);
if (pickedFile == null) return;
File file = File(pickedFile.path);
setState(() => _imageFile = file);
await uploadPhoto(file);
} catch (e) {
_showSnack('Gagal memilih foto: $e', Colors.red);
}
}
Future<void> uploadPhoto(File image) async {
setState(() => isProcessing = true);
try {
final result = await AuthService.uploadPhoto(image);
if (!mounted) return;
if (result['status'] == true) {
setState(() {
userData['foto'] = result['data']['foto'];
_imageFile = null;
});
_showSnack("Foto profil berhasil diperbarui", Colors.green);
} else {
_showSnack("Upload gagal: ${result['message'] ?? 'Coba lagi'}", Colors.red);
}
} catch (e) {
if (!mounted) return;
_showSnack("Error: $e", Colors.red);
} finally {
if (mounted) setState(() => isProcessing = false);
}
}
Future<void> deletePhoto() async {
setState(() => isProcessing = true);
try {
final result = await AuthService.deletePhoto();
if (!mounted) return;
if (result['status'] == true) {
setState(() {
userData['foto'] = null;
_imageFile = null;
});
_showSnack("Foto profil berhasil dihapus", Colors.green);
} else {
_showSnack("Gagal hapus foto: ${result['message'] ?? 'Coba lagi'}", Colors.red);
}
} catch (e) {
_showSnack("Error: $e", Colors.red);
} finally {
if (mounted) setState(() => isProcessing = false);
}
}
void _showSnack(String msg, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), backgroundColor: color),
);
}
Widget buildLoading() {
if (!isProcessing) return const SizedBox();
return Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(
// FIX: replaced withOpacity with withValues
color: Colors.black.withValues(alpha: 0.25),
child: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
backgroundColor: _bgPage,
body: Stack(
children: [
SingleChildScrollView(
child: Column(children: [_buildHeader(), _buildCard()]),
),
buildLoading(),
],
),
);
}
Widget _buildHeader() {
return Container(
width: double.infinity,
height: 300,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/gedunga3.jpg'),
fit: BoxFit.cover,
),
),
child: Container(
// FIX: replaced withOpacity with withValues
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5)),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 16),
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const Text("Foto Profil",
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
ListTile(
leading:
const Icon(Icons.image_rounded, color: _blue),
title: const Text("Ganti Foto"),
onTap: () {
Navigator.pop(context);
pickImage();
},
),
if (userData['foto'] != null && userData['foto'] != '')
ListTile(
leading: const Icon(Icons.delete_rounded,
color: Colors.red),
title: const Text("Hapus Foto",
style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(context);
_confirmDeletePhoto();
},
),
const SizedBox(height: 8),
],
),
);
},
);
},
child: Stack(
children: [
Container(
width: 98,
height: 98,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [_blue, Color(0xFF60A5FA), Color(0xFF93C5FD)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
Positioned(
top: 3,
left: 3,
right: 3,
bottom: 3,
child: CircleAvatar(
backgroundColor: const Color(0xFF1E3A5F),
backgroundImage: _imageFile != null
? FileImage(_imageFile!)
: (userData['foto'] != null && userData['foto'] != '')
? NetworkImage(userData['foto'])
: null,
child: (_imageFile == null &&
(userData['foto'] == null ||
userData['foto'] == ''))
? const Icon(Icons.person_rounded,
size: 44, color: _blue)
: null,
),
),
Positioned(
bottom: 2,
right: 2,
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blue,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.edit_rounded,
size: 12, color: Colors.white),
),
),
],
),
),
const SizedBox(height: 14),
Text(
userData['name'] ?? '-',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
const SizedBox(height: 4),
Text(
userData['email'] ?? '-',
// FIX: replaced withOpacity with withValues
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6), fontSize: 13),
),
const SizedBox(height: 12),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
decoration: BoxDecoration(
// FIX: replaced withOpacity with withValues
color: _blue.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(20),
border: Border.all(
// FIX: replaced withOpacity with withValues
color: _blue.withValues(alpha: 0.55), width: 0.5),
),
child: const Text(
'PETUGAS',
style: TextStyle(
color: Color(0xFF93C5FD),
fontSize: 10.5,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
),
],
),
),
),
);
}
Widget _buildCard() {
return Transform.translate(
offset: const Offset(0, -28),
child: Container(
decoration: const BoxDecoration(
color: _bgPage,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
margin: const EdgeInsets.only(top: 10, bottom: 20),
width: 38,
height: 4,
decoration: BoxDecoration(
// FIX: replaced withOpacity with withValues
color: Colors.black.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(2),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
_buildInfoTile(
'Nama', userData['name'] ?? '-',
const Color(0xFFEFF6FF), _blue),
const SizedBox(width: 10),
_buildInfoTile(
'No HP', userData['no_hp'] ?? '-',
const Color(0xFFF0FDFA), const Color(0xFF0D9488)),
],
),
),
const SizedBox(height: 24),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 22),
child: Text(
'PENGATURAN AKUN',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _textHint,
letterSpacing: 1.3,
),
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_buildMenuItem(
icon: Icons.person_outline_rounded,
iconBg: const Color(0xFFEFF6FF),
iconColor: _blue,
title: 'Edit Profil',
subtitle: userData['name'] ?? '-',
onTap: editProfile,
),
_buildMenuItem(
icon: Icons.email_outlined,
iconBg: const Color(0xFFF0FDFA),
iconColor: const Color(0xFF0D9488),
title: 'Ubah Email',
subtitle: userData['email'] ?? '-',
onTap: changeEmail,
),
_buildMenuItem(
icon: Icons.lock_outline_rounded,
iconBg: const Color(0xFFFFF7ED),
iconColor: const Color(0xFFF97316),
title: 'Ubah Password',
subtitle: 'Diperbarui baru-baru ini',
onTap: changePassword,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Divider(color: _border, thickness: 0.5),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildMenuItem(
icon: Icons.logout_rounded,
iconBg: const Color(0xFFFEE2E2),
iconColor: const Color(0xFFEF4444),
title: 'Keluar Akun',
subtitle: 'Sesi akan dihapus',
onTap: confirmLogout,
isLogout: true,
),
),
const SizedBox(height: 32),
],
),
),
);
}
Widget _buildInfoTile(
String label, String value, Color bg, Color valueColor) {
return Expanded(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: _cardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: _border, width: 0.5),
boxShadow: [
BoxShadow(
// FIX: replaced withOpacity with withValues
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _textHint,
letterSpacing: 0.8,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: valueColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
required Color iconBg,
required Color iconColor,
required String title,
required String subtitle,
required VoidCallback onTap,
bool isLogout = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isLogout ? const Color(0xFFFFF5F5) : _cardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isLogout
// FIX: replaced withOpacity with withValues
? const Color(0xFFEF4444).withValues(alpha: 0.15)
: _border,
width: 0.5,
),
boxShadow: [
BoxShadow(
// FIX: replaced withOpacity with withValues
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconBg, borderRadius: BorderRadius.circular(13)),
child: Icon(icon, color: iconColor, size: 18),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
color: isLogout
? const Color(0xFFEF4444)
: _textPrimary,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: isLogout
? const Color(0xFFFCA5A5)
: _textHint,
),
),
],
),
),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: isLogout
? const Color(0xFFFEE2E2)
: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(9),
),
child: Icon(
Icons.chevron_right_rounded,
size: 16,
color: isLogout ? const Color(0xFFEF4444) : _textHint,
),
),
],
),
),
);
}
// ══════════════════════════════════════════════════════════════════
// SHARED WIDGETS
// ══════════════════════════════════════════════════════════════════
Widget _styledField({
required TextEditingController controller,
required String label,
required IconData prefixIcon,
Color prefixColor = _blue,
bool obscure = false,
bool showToggle = false,
bool toggleValue = false,
VoidCallback? onToggle,
String? hint,
TextInputType? keyboardType,
int? maxLength,
ValueChanged<String>? onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _textSecondary,
letterSpacing: 0.3,
),
),
const SizedBox(height: 6),
TextField(
controller: controller,
obscureText: obscure && !toggleValue,
keyboardType: keyboardType,
maxLength: maxLength,
onChanged: onChanged,
style: const TextStyle(fontSize: 13, color: _textPrimary),
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: _textHint, fontSize: 13),
prefixIcon: Icon(prefixIcon, color: prefixColor, size: 18),
counterText: '',
suffixIcon: showToggle
? IconButton(
icon: Icon(
toggleValue
? Icons.visibility_rounded
: Icons.visibility_off_rounded,
color: _textHint,
size: 18,
),
onPressed: onToggle,
)
: null,
filled: true,
fillColor: const Color(0xFFFAFBFC),
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _border, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _border, width: 1.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _blue, width: 1.5),
),
),
),
],
);
}
Widget _dialogShell({
required Color iconBg,
required Color iconColor,
required IconData icon,
required String title,
required String subtitle,
required List<Widget> fields,
required String saveLabel,
required VoidCallback onSave,
Color? saveBtnColor,
VoidCallback? onCancel,
}) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 0,
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: iconBg,
borderRadius: BorderRadius.circular(12)),
child: Icon(icon, color: iconColor, size: 18),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: _textPrimary)),
Text(subtitle,
style: const TextStyle(
fontSize: 11.5, color: _textHint)),
],
),
],
),
const SizedBox(height: 20),
...fields,
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: onCancel ?? () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: const BorderSide(color: _border, width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('Batal',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _textSecondary)),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: onSave,
style: ElevatedButton.styleFrom(
backgroundColor: saveBtnColor ?? _blue,
padding: const EdgeInsets.symmetric(vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text(saveLabel,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.white)),
),
),
],
),
],
),
),
);
}
// ══════════════════════════════════════════════════════════════════
// DIALOGS
// ══════════════════════════════════════════════════════════════════
void _confirmDeletePhoto() {
showDialog(
context: context,
builder: (_) => Dialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
color: Color(0xFFFEE2E2), shape: BoxShape.circle),
child: const Icon(Icons.delete_forever_rounded,
color: Colors.red, size: 30),
),
const SizedBox(height: 16),
const Text("Hapus Foto?",
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
const Text(
"Foto profil kamu akan dihapus secara permanen.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
deletePhoto();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
child: const Text("Hapus"),
),
),
],
),
],
),
),
),
);
}
void editProfile() {
final nameController =
TextEditingController(text: userData['name']);
final phoneController =
TextEditingController(text: userData['no_hp'] ?? '');
showDialog(
context: context,
builder: (_) => _dialogShell(
icon: Icons.person_outline_rounded,
iconBg: const Color(0xFFEFF6FF),
iconColor: _blue,
title: 'Edit Profil',
subtitle: 'Perbarui nama dan nomor HP',
saveLabel: 'Simpan',
fields: [
_styledField(
controller: nameController,
label: 'Nama Lengkap',
prefixIcon: Icons.person_outline_rounded,
),
const SizedBox(height: 14),
_styledField(
controller: phoneController,
label: 'No HP',
prefixIcon: Icons.phone_outlined,
prefixColor: const Color(0xFF0D9488),
hint: 'Nomor HP kamu...',
),
],
onSave: () async {
setState(() => isProcessing = true);
final result = await AuthService.updateProfile(
name: nameController.text,
email: userData['email'],
noHp: phoneController.text.isEmpty
? null
: phoneController.text,
);
setState(() => isProcessing = false);
if (result['success']) {
setState(() {
userData['name'] = nameController.text;
userData['no_hp'] = phoneController.text;
});
if (!mounted) return;
Navigator.pop(context);
_showSnack('Profil berhasil diperbarui', Colors.green);
}
},
),
);
}
// ══════════════════════════════════════════════════════════════════
// UBAH EMAIL — dengan alur OTP
// ══════════════════════════════════════════════════════════════════
void changeEmail() {
final emailController = TextEditingController();
showDialog(
context: context,
builder: (_) => _dialogShell(
icon: Icons.email_outlined,
iconBg: const Color(0xFFF0FDFA),
iconColor: const Color(0xFF0D9488),
title: 'Ubah Email',
subtitle: 'Kode OTP akan dikirim ke email baru',
saveLabel: 'Kirim OTP',
fields: [
// Badge email aktif
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(8),
border:
Border.all(color: const Color(0xFF86EFAC), width: 0.5),
),
child: Row(
children: [
const Icon(Icons.check_circle_rounded,
size: 14, color: Color(0xFF16A34A)),
const SizedBox(width: 6),
Flexible(
child: Text(
'Email aktif: ${userData['email'] ?? '-'}',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF166534),
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 14),
_styledField(
controller: emailController,
label: 'Email Baru',
prefixIcon: Icons.email_outlined,
prefixColor: const Color(0xFF0D9488),
hint: 'Masukkan email baru...',
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 6),
const Row(
children: [
Icon(Icons.send_rounded, size: 12, color: _textHint),
SizedBox(width: 4),
Text('Kode OTP 6 digit dikirim ke email baru',
style: TextStyle(fontSize: 10.5, color: _textHint)),
],
),
],
onSave: () async {
final newEmail = emailController.text.trim();
if (newEmail.isEmpty) {
_showSnack('Email tidak boleh kosong', Colors.red);
return;
}
setState(() => isProcessing = true);
final result = await AuthService.sendEmailOtp(newEmail);
setState(() => isProcessing = false);
if (!mounted) return;
if (result['success'] == true) {
Navigator.pop(context);
_showOtpDialog(newEmail);
} else {
_showSnack(
result['message'] ?? 'Gagal mengirim OTP, coba lagi',
Colors.red,
);
}
},
),
);
}
// ── Dialog OTP ──────────────────────────────────────────────────
void _showOtpDialog(String newEmail) {
final List<TextEditingController> otpControllers =
List.generate(6, (_) => TextEditingController());
final List<FocusNode> focusNodes =
List.generate(6, (_) => FocusNode());
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
// isSending must live outside StatefulBuilder.builder so that
// setStateDialog(() => isSending = ...) actually triggers a rebuild.
bool isSending = false;
return StatefulBuilder(
builder: (context, setStateDialog) {
String getOtpCode() =>
otpControllers.map((c) => c.text).join();
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24)),
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFFF0FDFA),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.mark_email_read_outlined,
color: Color(0xFF0D9488), size: 26),
),
const SizedBox(height: 14),
const Text(
'Verifikasi Email',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: _textPrimary),
),
const SizedBox(height: 6),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: const TextStyle(
fontSize: 12.5,
color: _textHint,
height: 1.5),
children: [
const TextSpan(text: 'Kode OTP telah dikirim ke\n'),
TextSpan(
text: newEmail,
style: const TextStyle(
color: Color(0xFF0D9488),
fontWeight: FontWeight.w600),
),
],
),
),
const SizedBox(height: 24),
// 6 kotak OTP
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(6, (i) {
return SizedBox(
width: 42,
height: 50,
child: TextField(
controller: otpControllers[i],
focusNode: focusNodes[i],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: _textPrimary),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: _border, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: _border, width: 1.5),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: _blue, width: 2),
),
),
onChanged: (val) {
if (val.isNotEmpty && i < 5) {
FocusScope.of(context)
.requestFocus(focusNodes[i + 1]);
} else if (val.isEmpty && i > 0) {
FocusScope.of(context)
.requestFocus(focusNodes[i - 1]);
}
setStateDialog(() {});
},
),
);
}),
),
const SizedBox(height: 20),
// Tombol Verifikasi
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isSending
? null
: () async {
final otp = getOtpCode();
if (otp.length < 6) {
_showSnack(
'Masukkan 6 digit kode OTP',
Colors.orange);
return;
}
setStateDialog(() => isSending = true);
final result =
await AuthService.verifyEmailOtp(
newEmail,
otp,
);
setStateDialog(() => isSending = false);
// FIX: use_build_context_synchronously —
// check mounted on the State before using
// this.context after the await gap.
if (!mounted) return;
if (result['success'] == true) {
setState(
() => userData['email'] = newEmail);
// ignore: use_build_context_synchronously
Navigator.pop(context);
_showSnack('Email berhasil diperbarui',
Colors.green);
} else {
_showSnack(
result['message'] ??
'Kode OTP salah atau sudah kadaluarsa',
Colors.red,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D9488),
padding:
const EdgeInsets.symmetric(vertical: 13),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: isSending
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white),
)
: const Text(
'Verifikasi',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white),
),
),
),
const SizedBox(height: 10),
// Kirim Ulang + Batal
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () async {
final result =
await AuthService.sendEmailOtp(newEmail);
if (!mounted) return;
if (result['success'] == true) {
_showSnack(
'OTP baru telah dikirim', Colors.blue);
} else {
_showSnack(
'Gagal mengirim ulang OTP', Colors.red);
}
},
child: const Text(
'Kirim Ulang OTP',
style: TextStyle(
fontSize: 12,
color: _blue,
fontWeight: FontWeight.w600),
),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal',
style: TextStyle(
fontSize: 12, color: _textHint)),
),
],
),
],
),
),
);
},
);
},
);
}
void changePassword() {
final oldPass = TextEditingController();
final newPass = TextEditingController();
final confirmPass = TextEditingController();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setStateDialog) => _dialogShell(
icon: Icons.lock_outline_rounded,
iconBg: const Color(0xFFFFF7ED),
iconColor: const Color(0xFFF97316),
title: 'Ubah Password',
subtitle: 'Gunakan password yang kuat',
saveLabel: 'Perbarui',
saveBtnColor: const Color(0xFFEF4444),
fields: [
_styledField(
controller: oldPass,
label: 'Password Lama',
prefixIcon: Icons.lock_outline_rounded,
prefixColor: const Color(0xFFF97316),
obscure: true,
showToggle: true,
toggleValue: showOldPassword,
onToggle: () =>
setStateDialog(() => showOldPassword = !showOldPassword),
),
const SizedBox(height: 14),
_styledField(
controller: newPass,
label: 'Password Baru',
prefixIcon: Icons.lock_outline_rounded,
prefixColor: _blue,
obscure: true,
showToggle: true,
toggleValue: showNewPassword,
onToggle: () => setStateDialog(
() => showNewPassword = !showNewPassword),
),
const SizedBox(height: 14),
_styledField(
controller: confirmPass,
label: 'Konfirmasi Password',
prefixIcon: Icons.check_circle_outline_rounded,
prefixColor: const Color(0xFF22C55E),
obscure: true,
showToggle: true,
toggleValue: showConfirmPassword,
onToggle: () => setStateDialog(
() => showConfirmPassword = !showConfirmPassword),
),
],
onSave: () async {
if (newPass.text != confirmPass.text) {
_showSnack('Password tidak sama', Colors.red);
return;
}
// Capture navigator before the async gap to avoid
// use_build_context_synchronously warning.
final nav = Navigator.of(context);
setState(() => isProcessing = true);
final result = await AuthService.updatePassword(
oldPass.text, newPass.text);
setState(() => isProcessing = false);
if (result['success']) {
if (!mounted) return;
nav.pop();
_showSnack('Password berhasil diperbarui', Colors.green);
}
},
),
),
);
}
void confirmLogout() {
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24)),
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 56,
height: 56,
decoration: const BoxDecoration(
color: Color(0xFFFEE2E2), shape: BoxShape.circle),
child: const Icon(Icons.logout_rounded,
color: Color(0xFFEF4444), size: 26),
),
const SizedBox(height: 16),
const Text('Keluar Akun?',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: _textPrimary)),
const SizedBox(height: 6),
const Text(
'Sesi kamu akan dihapus dan kamu\nharus login kembali.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.5, color: _textHint, height: 1.5),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
padding:
const EdgeInsets.symmetric(vertical: 12),
side: const BorderSide(
color: _border, width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('Batal',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _textSecondary)),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () async {
await AuthService.logout();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(
context,
AppRoutes.login,
(route) => false,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFEF4444),
padding:
const EdgeInsets.symmetric(vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('Keluar',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.white)),
),
),
],
),
],
),
),
),
);
}
}