1106 lines
40 KiB
Dart
1106 lines
40 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/warga_dashboard_controller.dart';
|
|
import '../views/warga_layout.dart';
|
|
import '../../../theme/app_colors.dart';
|
|
import '../../../widgets/app_drawer.dart';
|
|
import '../../../services/navigation_service.dart';
|
|
|
|
class WargaProfileView extends GetView<WargaDashboardController> {
|
|
const WargaProfileView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final navigationService = Get.find<NavigationService>();
|
|
navigationService.setNavIndex(2);
|
|
|
|
// State for editing mode
|
|
final isEditing = false.obs;
|
|
// State for avatar deletion
|
|
final isAvatarDeleted = false.obs;
|
|
|
|
// Text editing controllers for editable fields
|
|
final nameController = TextEditingController(
|
|
text: controller.userName.value,
|
|
);
|
|
final phoneController = TextEditingController(
|
|
text: controller.userPhone.value,
|
|
);
|
|
|
|
// Store original values for cancel functionality
|
|
final originalName = controller.userName.value;
|
|
final originalPhone = controller.userPhone.value;
|
|
|
|
return WargaLayout(
|
|
appBar: AppBar(
|
|
title: const Text('Profil Saya'),
|
|
backgroundColor: AppColors.primary,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
actions: [
|
|
Obx(
|
|
() =>
|
|
isEditing.value
|
|
? Row(
|
|
children: [
|
|
// Cancel button
|
|
IconButton(
|
|
onPressed: () {
|
|
// Reset values to original
|
|
nameController.text = originalName;
|
|
phoneController.text = originalPhone;
|
|
isEditing.value = false;
|
|
isAvatarDeleted.value =
|
|
false; // Reset avatar deletion state
|
|
controller
|
|
.cancelAvatarChange(); // Reset temporary avatar
|
|
},
|
|
icon: const Icon(Icons.close),
|
|
tooltip: 'Batal',
|
|
),
|
|
// Save button
|
|
IconButton(
|
|
onPressed: () async {
|
|
// Show loading indicator
|
|
final loadingDialog = Get.dialog(
|
|
const Center(child: CircularProgressIndicator()),
|
|
barrierDismissible: false,
|
|
);
|
|
|
|
bool success = true;
|
|
|
|
// Check if there's a new avatar to save
|
|
if (controller.tempAvatarBytes.value != null) {
|
|
// Save the new avatar
|
|
success = await controller.saveNewAvatar();
|
|
if (!success) {
|
|
// Close loading dialog if avatar saving fails
|
|
Get.back();
|
|
|
|
Get.snackbar(
|
|
'Gagal',
|
|
'Terjadi kesalahan saat menyimpan foto profil',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
|
|
isEditing.value = false;
|
|
return;
|
|
}
|
|
}
|
|
// If avatar was deleted (and no new avatar selected), update it in the database
|
|
else if (isAvatarDeleted.value) {
|
|
success = await controller.deleteUserAvatar();
|
|
if (!success) {
|
|
// Close loading dialog if avatar deletion fails
|
|
Get.back();
|
|
|
|
Get.snackbar(
|
|
'Gagal',
|
|
'Terjadi kesalahan saat menghapus foto profil',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
|
|
isAvatarDeleted.value =
|
|
false; // Reset avatar deletion state
|
|
isEditing.value = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Save profile changes to database
|
|
success = await controller.updateUserProfile(
|
|
namaLengkap: nameController.text,
|
|
noHp: phoneController.text,
|
|
);
|
|
|
|
// Close loading dialog
|
|
Get.back();
|
|
|
|
if (success) {
|
|
Get.snackbar(
|
|
'Sukses',
|
|
'Perubahan berhasil disimpan',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
);
|
|
} else {
|
|
Get.snackbar(
|
|
'Gagal',
|
|
'Terjadi kesalahan saat menyimpan perubahan',
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
|
|
// Reset to original values on failure
|
|
nameController.text = originalName;
|
|
phoneController.text = originalPhone;
|
|
isAvatarDeleted.value =
|
|
false; // Reset avatar deletion state
|
|
controller
|
|
.cancelAvatarChange(); // Reset temporary avatar
|
|
}
|
|
|
|
isEditing.value = false;
|
|
},
|
|
icon: const Icon(Icons.check),
|
|
tooltip: 'Simpan',
|
|
),
|
|
],
|
|
)
|
|
: IconButton(
|
|
onPressed: () {
|
|
isEditing.value = true;
|
|
// Update controllers with current values when entering edit mode
|
|
nameController.text = controller.userName.value;
|
|
phoneController.text = controller.userPhone.value;
|
|
},
|
|
icon: const Icon(Icons.edit_outlined),
|
|
tooltip: 'Edit Profil',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
drawer: AppDrawer(
|
|
onNavItemTapped: (index) {
|
|
// Handle navigation if needed
|
|
},
|
|
onLogout: () {
|
|
controller.logout();
|
|
},
|
|
),
|
|
backgroundColor: Colors.grey.shade100,
|
|
body: RefreshIndicator(
|
|
color: AppColors.primary,
|
|
onRefresh: () async {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
controller.refreshData();
|
|
// Update text controllers with refreshed data
|
|
nameController.text = controller.userName.value;
|
|
phoneController.text = controller.userPhone.value;
|
|
isAvatarDeleted.value = false; // Reset avatar deletion state
|
|
return;
|
|
},
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: Column(
|
|
children: [
|
|
Obx(
|
|
() => _buildProfileHeader(
|
|
context,
|
|
isEditing.value,
|
|
isAvatarDeleted,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Obx(
|
|
() => _buildPersonalInfoCard(
|
|
context,
|
|
isEditing.value,
|
|
nameController,
|
|
phoneController,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildSettingsCard(context),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProfileHeader(
|
|
BuildContext context,
|
|
bool isEditing,
|
|
RxBool isAvatarDeleted,
|
|
) {
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
AppColors.primary,
|
|
AppColors.primary.withBlue(AppColors.primary.blue + 30),
|
|
],
|
|
),
|
|
borderRadius: const BorderRadius.only(
|
|
bottomLeft: Radius.circular(30),
|
|
bottomRight: Radius.circular(30),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 36),
|
|
child: Column(
|
|
children: [
|
|
// Profile picture with shadow effect
|
|
Obx(() {
|
|
final avatarUrl = controller.userAvatar.value;
|
|
final shouldShowFallback =
|
|
isAvatarDeleted.value ||
|
|
avatarUrl == null ||
|
|
avatarUrl.isEmpty;
|
|
|
|
// Check if there's a temporary avatar preview
|
|
final hasTemporaryAvatar =
|
|
controller.tempAvatarBytes.value != null;
|
|
|
|
return Column(
|
|
children: [
|
|
Stack(
|
|
alignment: Alignment.bottomRight,
|
|
children: [
|
|
Container(
|
|
height: 110,
|
|
width: 110,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white, width: 4),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.2),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(55),
|
|
child:
|
|
hasTemporaryAvatar
|
|
// Show temporary avatar preview
|
|
? Image.memory(
|
|
controller.tempAvatarBytes.value!,
|
|
fit: BoxFit.cover,
|
|
)
|
|
: shouldShowFallback
|
|
? _buildAvatarFallback()
|
|
: Image.network(
|
|
avatarUrl!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder:
|
|
(context, error, stackTrace) =>
|
|
_buildAvatarFallback(),
|
|
loadingBuilder: (context, child, progress) {
|
|
if (progress == null) return child;
|
|
return _buildAvatarFallback();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
if (isEditing)
|
|
GestureDetector(
|
|
onTap: () {
|
|
// Show image source dialog when camera icon is tapped
|
|
controller.showImageSourceDialog();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: AppColors.primary,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: Icon(
|
|
Icons.camera_alt,
|
|
size: 18,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Image selection buttons when in edit mode
|
|
if (isEditing)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Show "Batal" button if temporary avatar is selected
|
|
if (hasTemporaryAvatar)
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
controller.cancelAvatarChange();
|
|
},
|
|
icon: const Icon(Icons.close, size: 16),
|
|
label: const Text('Batal'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey.shade200,
|
|
foregroundColor: Colors.grey.shade800,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
textStyle: const TextStyle(fontSize: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
if (hasTemporaryAvatar) {
|
|
// If temporary avatar exists, don't show the snackbar
|
|
// The actual saving will happen when the user presses the save button
|
|
isAvatarDeleted.value = false;
|
|
} else {
|
|
// Set avatar deleted flag
|
|
isAvatarDeleted.value = true;
|
|
}
|
|
},
|
|
icon: const Icon(Icons.delete_outline, size: 16),
|
|
label: const Text('Hapus'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red.shade50,
|
|
foregroundColor: Colors.red.shade700,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
textStyle: const TextStyle(fontSize: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
const SizedBox(height: 16),
|
|
// User name with subtle text shadow
|
|
Obx(
|
|
() => Text(
|
|
controller.userName.value,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black26,
|
|
blurRadius: 2,
|
|
offset: Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
// User role in a stylish chip
|
|
Obx(
|
|
() => Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(30),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.5),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.verified_user,
|
|
size: 14,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
controller.userRole.value,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(0.9),
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPersonalInfoCard(
|
|
BuildContext context,
|
|
bool isEditing,
|
|
TextEditingController nameController,
|
|
TextEditingController phoneController,
|
|
) {
|
|
return Column(
|
|
children: [
|
|
// Section 1: Data Diri - Dengan desain yang lebih modern
|
|
Card(
|
|
elevation: 2,
|
|
shadowColor: AppColors.primary.withOpacity(0.3),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Colors.white, AppColors.primary.withOpacity(0.05)],
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
Icons.person_rounded,
|
|
color: AppColors.primary,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Data Diri',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
Text(
|
|
'Informasi personal Anda',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (isEditing) ...[
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.edit_note,
|
|
color: Colors.orange.shade700,
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'Mode Edit',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.orange.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
// Email - always read-only
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.email_rounded,
|
|
title: 'Email',
|
|
value: controller.userEmail.value,
|
|
isVerified: true,
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Nama Lengkap - editable
|
|
_buildEditableInfoItem(
|
|
context,
|
|
icon: Icons.person_rounded,
|
|
title: 'Nama Lengkap',
|
|
value: controller.userName.value,
|
|
isEditing: isEditing,
|
|
controller: nameController,
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Nomor Telepon - editable
|
|
_buildEditableInfoItem(
|
|
context,
|
|
icon: Icons.phone_rounded,
|
|
title: 'Nomor Telepon',
|
|
value: controller.userPhone.value,
|
|
isEditing: isEditing,
|
|
controller: phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Section 2: Informasi Warga - Dengan desain yang lebih modern
|
|
Card(
|
|
elevation: 2,
|
|
shadowColor: AppColors.accent.withOpacity(0.3),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Colors.white, AppColors.accent.withOpacity(0.05)],
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.accent.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
Icons.badge_rounded,
|
|
color: AppColors.accent,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Informasi Warga',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.accent,
|
|
),
|
|
),
|
|
Text(
|
|
'Detail kependudukan',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.credit_card_rounded,
|
|
title: 'NIK',
|
|
value: controller.userNik.value,
|
|
isImportant: true,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.calendar_today_rounded,
|
|
title: 'Tanggal Lahir',
|
|
value: formatTanggalLahir(
|
|
controller.userTanggalLahir.value,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.home_rounded,
|
|
title: 'Alamat',
|
|
value: controller.userAddress.value,
|
|
isMultiLine: true,
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Tampilkan RT/RW dan Kelurahan/Desa dalam dua baris terpisah
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.location_on_rounded,
|
|
title: 'RT/RW',
|
|
value: controller.userRtRw.value,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.location_city_rounded,
|
|
title: 'Kelurahan/Desa',
|
|
value: controller.userKelurahanDesa.value,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInfoItemModern(
|
|
context,
|
|
icon: Icons.map_rounded,
|
|
title: 'Kecamatan',
|
|
value: controller.userKecamatan.value,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoItemModern(
|
|
BuildContext context, {
|
|
required IconData icon,
|
|
required String title,
|
|
required String value,
|
|
bool isMultiLine = false,
|
|
bool isImportant = false,
|
|
bool isVerified = false,
|
|
bool isCompact = false,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color:
|
|
isImportant
|
|
? AppColors.primary.withOpacity(0.3)
|
|
: Colors.grey.shade200,
|
|
width: isImportant ? 1.5 : 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.05),
|
|
blurRadius: 5,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: isCompact ? 12 : 16,
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment:
|
|
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(isCompact ? 6 : 8),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
isImportant
|
|
? AppColors.primary.withOpacity(0.15)
|
|
: AppColors.accent.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: isImportant ? AppColors.primary : AppColors.accent,
|
|
size: isCompact ? 16 : 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: isCompact ? 12 : 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
if (isVerified) ...[
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.verified, size: 14, color: Colors.green),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
value.isEmpty ? 'Tidak tersedia' : value,
|
|
style: TextStyle(
|
|
fontSize: isCompact ? 14 : 15,
|
|
fontWeight: isImportant ? FontWeight.w700 : FontWeight.w600,
|
|
color:
|
|
isImportant
|
|
? AppColors.primary
|
|
: value.isEmpty
|
|
? Colors.grey.shade400
|
|
: Colors.grey.shade800,
|
|
),
|
|
maxLines: isMultiLine ? 3 : 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isImportant)
|
|
Icon(
|
|
Icons.priority_high,
|
|
size: 16,
|
|
color: AppColors.primary.withOpacity(0.7),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEditableInfoItem(
|
|
BuildContext context, {
|
|
required IconData icon,
|
|
required String title,
|
|
required String value,
|
|
required bool isEditing,
|
|
required TextEditingController controller,
|
|
TextInputType keyboardType = TextInputType.text,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: isEditing ? Colors.white : Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color:
|
|
isEditing
|
|
? AppColors.primary.withOpacity(0.5)
|
|
: Colors.grey.shade200,
|
|
),
|
|
boxShadow:
|
|
isEditing
|
|
? [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
padding: EdgeInsets.all(isEditing ? 12 : 16),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(icon, color: AppColors.primary, size: 20),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (isEditing)
|
|
TextField(
|
|
controller: controller,
|
|
keyboardType: keyboardType,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
|
border: InputBorder.none,
|
|
hintText: 'Masukkan $title',
|
|
hintStyle: TextStyle(
|
|
color: Colors.grey.shade400,
|
|
fontSize: 15,
|
|
),
|
|
),
|
|
)
|
|
else
|
|
Text(
|
|
value.isEmpty ? 'Tidak tersedia' : value,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isEditing) Icon(Icons.edit, size: 16, color: AppColors.primary),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsCard(BuildContext context) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.settings_rounded,
|
|
color: AppColors.primary,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
'Pengaturan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
_buildActionItem(
|
|
icon: Icons.lock_outline_rounded,
|
|
title: 'Ubah Password',
|
|
iconColor: AppColors.primary,
|
|
onTap: () {
|
|
Get.snackbar(
|
|
'Info',
|
|
'Fitur Ubah Password akan segera tersedia',
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
},
|
|
),
|
|
Divider(height: 1, color: Colors.grey.shade200),
|
|
_buildActionItem(
|
|
icon: Icons.logout_rounded,
|
|
title: 'Keluar',
|
|
iconColor: Colors.red.shade400,
|
|
isDestructive: true,
|
|
onTap: () {
|
|
_showLogoutConfirmation(context);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionItem({
|
|
required IconData icon,
|
|
required String title,
|
|
required VoidCallback onTap,
|
|
Color? iconColor,
|
|
bool isDestructive = false,
|
|
}) {
|
|
final color =
|
|
isDestructive ? Colors.red.shade400 : iconColor ?? AppColors.primary;
|
|
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(icon, color: color, size: 20),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color:
|
|
isDestructive ? Colors.red.shade400 : Colors.grey.shade800,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Icon(Icons.chevron_right, color: Colors.grey.shade400, size: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showLogoutConfirmation(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
title: const Text('Konfirmasi Keluar'),
|
|
content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.grey.shade700,
|
|
),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
controller.logout();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red.shade400,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text('Keluar'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAvatarFallback() {
|
|
return Container(
|
|
color: Colors.grey.shade200,
|
|
child: Center(
|
|
child: Icon(
|
|
Icons.person,
|
|
color: AppColors.primary.withOpacity(0.7),
|
|
size: 50,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Fungsi untuk memformat tanggal lahir ke format Indonesia
|
|
String formatTanggalLahir(String tanggal) {
|
|
if (tanggal.isEmpty) return '';
|
|
|
|
try {
|
|
// Coba parse tanggal dari berbagai format yang mungkin
|
|
DateTime? dateTime;
|
|
|
|
// Coba format yyyy-MM-dd
|
|
if (tanggal.contains('-')) {
|
|
final parts = tanggal.split('-');
|
|
if (parts.length == 3) {
|
|
dateTime = DateTime.tryParse(tanggal);
|
|
}
|
|
}
|
|
// Coba format dd/MM/yyyy
|
|
else if (tanggal.contains('/')) {
|
|
final parts = tanggal.split('/');
|
|
if (parts.length == 3) {
|
|
dateTime = DateTime.tryParse('${parts[2]}-${parts[1]}-${parts[0]}');
|
|
}
|
|
}
|
|
|
|
// Jika tidak bisa parse, kembalikan nilai asli
|
|
if (dateTime == null) return tanggal;
|
|
|
|
// Daftar nama bulan dalam bahasa Indonesia
|
|
final List<String> namaBulan = [
|
|
'Januari',
|
|
'Februari',
|
|
'Maret',
|
|
'April',
|
|
'Mei',
|
|
'Juni',
|
|
'Juli',
|
|
'Agustus',
|
|
'September',
|
|
'Oktober',
|
|
'November',
|
|
'Desember',
|
|
];
|
|
|
|
// Format tanggal ke format Indonesia: dd Bulan yyyy
|
|
return '${dateTime.day} ${namaBulan[dateTime.month - 1]} ${dateTime.year}';
|
|
} catch (e) {
|
|
// Jika ada error, kembalikan nilai asli
|
|
return tanggal;
|
|
}
|
|
}
|
|
}
|