semua fitur selesai
This commit is contained in:
@ -13,6 +13,24 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
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'),
|
||||
@ -20,16 +38,128 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit profil akan segera tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Edit Profil',
|
||||
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.BOTTOM,
|
||||
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.BOTTOM,
|
||||
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.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menyimpan perubahan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
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;
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Edit Profil',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -47,15 +177,32 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
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: [
|
||||
_buildProfileHeader(context),
|
||||
Obx(
|
||||
() => _buildProfileHeader(
|
||||
context,
|
||||
isEditing.value,
|
||||
isAvatarDeleted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoCard(context),
|
||||
Obx(
|
||||
() => _buildPersonalInfoCard(
|
||||
context,
|
||||
isEditing.value,
|
||||
nameController,
|
||||
phoneController,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSettingsCard(context),
|
||||
const SizedBox(height: 24),
|
||||
@ -66,7 +213,11 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileHeader(BuildContext context) {
|
||||
Widget _buildProfileHeader(
|
||||
BuildContext context,
|
||||
bool isEditing,
|
||||
RxBool isAvatarDeleted,
|
||||
) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
@ -97,37 +248,153 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
// Profile picture with shadow effect
|
||||
Obx(() {
|
||||
final avatarUrl = controller.userAvatar.value;
|
||||
return 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:
|
||||
avatarUrl != null && avatarUrl.isNotEmpty
|
||||
? Image.network(
|
||||
avatarUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
_buildAvatarFallback(),
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return _buildAvatarFallback();
|
||||
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;
|
||||
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Foto profil akan dihapus setelah menekan Simpan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: _buildAvatarFallback(),
|
||||
),
|
||||
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),
|
||||
@ -193,84 +460,192 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Row(
|
||||
Widget _buildPersonalInfoCard(
|
||||
BuildContext context,
|
||||
bool isEditing,
|
||||
TextEditingController nameController,
|
||||
TextEditingController phoneController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
// Section 1: Data Diri
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.person_outline, color: AppColors.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'INFORMASI PERSONAL',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Data Diri',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
if (isEditing) ...[
|
||||
const Spacer(),
|
||||
Text(
|
||||
'Mode Edit',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.edit_note,
|
||||
color: Colors.orange.shade700,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Email - always read-only
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.email_rounded,
|
||||
title: 'Email',
|
||||
value: controller.userEmail.value,
|
||||
),
|
||||
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 Divider(height: 1),
|
||||
_buildInfoItem(
|
||||
icon: Icons.email_outlined,
|
||||
title: 'Email',
|
||||
value:
|
||||
controller.userEmail.value.isEmpty
|
||||
? 'emailpengguna@example.com'
|
||||
: controller.userEmail.value,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section 2: Informasi Warga
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.credit_card_outlined,
|
||||
title: 'NIK',
|
||||
value:
|
||||
controller.userNik.value.isEmpty
|
||||
? '123456789012345'
|
||||
: controller.userNik.value,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.badge_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Informasi Warga',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.credit_card_rounded,
|
||||
title: 'NIK',
|
||||
value: controller.userNik.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.calendar_today_rounded,
|
||||
title: 'Tanggal Lahir',
|
||||
value: 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),
|
||||
_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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.phone_outlined,
|
||||
title: 'Nomor Telepon',
|
||||
value:
|
||||
controller.userPhone.value.isEmpty
|
||||
? '081234567890'
|
||||
: controller.userPhone.value,
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.home_outlined,
|
||||
title: 'Alamat Lengkap',
|
||||
value:
|
||||
controller.userAddress.value.isEmpty
|
||||
? 'Jl. Contoh No. 123, Desa Sejahtera, Kec. Makmur, Kab. Berkah, Prov. Damai'
|
||||
: controller.userAddress.value,
|
||||
isMultiLine: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem({
|
||||
Widget _buildInfoItemModern(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String value,
|
||||
bool isMultiLine = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||
@ -290,14 +665,18 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
value.isEmpty ? 'Tidak tersedia' : value,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
maxLines: isMultiLine ? 3 : 1,
|
||||
@ -311,33 +690,123 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
Icons.settings_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 18,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'PENGATURAN',
|
||||
'Pengaturan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -345,7 +814,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildActionItem(
|
||||
icon: Icons.lock_outline,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
title: 'Ubah Password',
|
||||
iconColor: AppColors.primary,
|
||||
onTap: () {
|
||||
@ -358,7 +827,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildActionItem(
|
||||
icon: Icons.logout,
|
||||
icon: Icons.logout_rounded,
|
||||
title: 'Keluar',
|
||||
iconColor: Colors.red.shade400,
|
||||
isDestructive: true,
|
||||
@ -384,7 +853,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
@ -395,7 +864,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
|
Reference in New Issue
Block a user