Perbarui dependensi dengan menambahkan paket percent_indicator versi 4.2.4. Modifikasi file pubspec.yaml dan pubspec.lock untuk mencerminkan perubahan ini. Selain itu, perbarui status penerimaan di PelaksanaanPenyaluranController dari 'SUDAHMENERIMA' menjadi 'DITERIMA' untuk konsistensi. Tambahkan fungsionalitas baru di PetugasDesaDashboardController untuk memuat jadwal hari ini dan total penitipan terverifikasi. Perbarui tampilan di beberapa view untuk meningkatkan pengalaman pengguna dan konsistensi data.

This commit is contained in:
Khafidh Fuadi
2025-03-25 21:03:40 +07:00
parent 32736be867
commit 3b963178f4
20 changed files with 2191 additions and 818 deletions

View File

@ -46,6 +46,9 @@ class ProfileController extends GetxController {
Future<void> loadUserData() async {
isLoading.value = true;
try {
// Hapus cache data user sebelum mengambil data baru
_supabaseService.clearUserProfileCache();
// Mendapatkan data user dari service
final userData = await _supabaseService.getUserProfile();
if (userData != null) {
@ -67,9 +70,15 @@ class ProfileController extends GetxController {
if (roleData.value?['foto_profil'] != null) {
fotoProfil.value = roleData.value?['foto_profil'] ?? '';
print(fotoProfil.value);
} else {
// Reset foto profil jika tidak ada data
fotoProfil.value = '';
}
}
}
// Muat ulang data user di AuthController untuk memastikan konsistensi
await _authController.refreshUserData();
} catch (e) {
Get.snackbar(
'Error',
@ -140,6 +149,95 @@ class ProfileController extends GetxController {
// Metode untuk menghapus foto profil
void clearFotoProfil() {
fotoProfilPath.value = '';
if (isEditing.value) {
// Cek jika user adalah warga
if (user.value?.role?.toLowerCase() == 'warga') {
Get.snackbar(
'Tidak Diizinkan',
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber,
colorText: Colors.black,
duration: const Duration(seconds: 5),
);
return;
}
// Tandai bahwa foto profil akan dihapus saat menyimpan perubahan
Get.dialog(
AlertDialog(
title: const Text('Konfirmasi'),
content: const Text('Apakah Anda yakin ingin menghapus foto profil?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () async {
Get.back();
try {
final userData = user.value;
if (userData == null) return;
// Update data profil dengan foto kosong
switch (userData.role?.toLowerCase() ?? 'unknown') {
case 'donatur':
await _supabaseService.updateDonaturProfile(
userId: userData.id,
nama: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil:
'', // Kosongkan foto profil dengan string kosong
);
break;
case 'petugas_desa':
await _supabaseService.updatePetugasDesaProfile(
userId: userData.id,
nama: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil:
'', // Kosongkan foto profil dengan string kosong
);
break;
default:
break;
}
// Hapus cache dan refresh data
_supabaseService.clearUserProfileCache();
fotoProfil.value = '';
await _authController.refreshUserData();
await loadUserData();
Get.snackbar(
'Sukses',
'Foto profil berhasil dihapus',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Error',
'Gagal menghapus foto profil: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Hapus'),
),
],
),
);
}
}
// Metode untuk mengupload foto profil
@ -179,11 +277,34 @@ class ProfileController extends GetxController {
return;
}
final userData = user.value;
if (userData == null) {
Get.snackbar(
'Error',
'Data user tidak ditemukan',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Cek jika user adalah warga, maka tidak diperbolehkan mengubah profil
if (userData.role?.toLowerCase() == 'warga') {
Get.snackbar(
'Tidak Diizinkan',
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber,
colorText: Colors.black,
duration: const Duration(seconds: 5),
);
isEditing.value = false;
return;
}
isLoading.value = true;
try {
final userData = user.value;
if (userData == null) throw 'Data user tidak ditemukan';
// Upload foto profil jika ada
String? fotoProfilUrl;
if (fotoProfilPath.isNotEmpty) {
@ -195,15 +316,6 @@ class ProfileController extends GetxController {
// Update data sesuai role
switch (userData.role?.toLowerCase() ?? 'unknown') {
case 'warga':
await _supabaseService.updateWargaProfile(
userId: userData.id,
namaLengkap: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil: fotoProfilUrl,
);
break;
case 'donatur':
await _supabaseService.updateDonaturProfile(
userId: userData.id,
@ -226,14 +338,17 @@ class ProfileController extends GetxController {
throw 'Role tidak valid';
}
// Refresh data lokal
await loadUserData();
// Hapus cache data profil sebelum refresh
_supabaseService.clearUserProfileCache();
// Reset path foto setelah update
fotoProfilPath.value = '';
// Refresh data di AuthController untuk menyebarkan perubahan ke seluruh aplikasi
await _authController.refreshUserData();
// Reset path foto setelah update
fotoProfilPath.value = '';
// Refresh data lokal
await loadUserData();
// Keluar dari mode edit
isEditing.value = false;

View File

@ -58,17 +58,22 @@ class ProfileView extends GetView<ProfileController> {
title: const Text('Profil'),
actions: [
Obx(() {
if (controller.isEditing.value) {
return IconButton(
icon: const Icon(Icons.save),
onPressed: controller.updateProfile,
);
} else {
return IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.toggleEditMode,
);
// Hanya tampilkan tombol edit jika user bukan warga
if (controller.user.value?.role?.toLowerCase() != 'warga') {
if (controller.isEditing.value) {
return IconButton(
icon: const Icon(Icons.save),
onPressed: controller.updateProfile,
);
} else {
return IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.toggleEditMode,
);
}
}
return const SizedBox
.shrink(); // Jangan tampilkan apapun untuk warga
}),
],
),
@ -122,7 +127,8 @@ class ProfileView extends GetView<ProfileController> {
// Tombol edit foto (hanya muncul dalam mode edit)
Obx(() {
if (controller.isEditing.value) {
if (controller.isEditing.value &&
controller.user.value?.role?.toLowerCase() != 'warga') {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
@ -164,13 +170,36 @@ class ProfileView extends GetView<ProfileController> {
}),
const SizedBox(height: 16),
Obx(() => Text(
controller.user.value?.name ?? 'Pengguna',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
)),
// Menggunakan Obx untuk reaktif memperbarui nama
Obx(() {
// Mengambil nama dari roleData jika ada, atau dari user
String displayName = '';
final roleDataValue = controller.roleData.value;
final userValue = controller.user.value;
if (roleDataValue != null) {
// Prioritaskan data dari roleData karena lebih spesifik
if (roleDataValue['nama_lengkap'] != null) {
displayName = roleDataValue['nama_lengkap'];
} else if (roleDataValue['nama'] != null) {
displayName = roleDataValue['nama'];
}
}
// Gunakan data dari user jika tidak ada di roleData
if (displayName.isEmpty && userValue != null) {
displayName = userValue.name ?? 'Pengguna';
}
return Text(
displayName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
}),
const SizedBox(height: 4),
Obx(() {
final role = controller.user.value?.role;
@ -235,8 +264,10 @@ class ProfileView extends GetView<ProfileController> {
fit: BoxFit.cover,
width: 120,
height: 120,
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
errorBuilder: (context, error, stackTrace) {
print('Error loading local image: $error');
return _buildDefaultProfileImage();
},
)
: Image.network(
imagePath,
@ -254,8 +285,11 @@ class ProfileView extends GetView<ProfileController> {
),
);
},
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
errorBuilder: (context, error, stackTrace) {
print('Error loading network image: $error');
print('Failed image URL: $imagePath');
return _buildDefaultProfileImage();
},
),
),
),
@ -321,157 +355,273 @@ class ProfileView extends GetView<ProfileController> {
return Obx(() {
final isEditing = controller.isEditing.value;
final user = controller.user.value;
// Form tidak bisa diedit jika usernya warga
final bool canEdit = isEditing && user?.role?.toLowerCase() != 'warga';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
),
const SizedBox(height: 16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person_outline, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
// Nama
TextField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
enabled: isEditing,
),
),
const SizedBox(height: 16),
// Menampilkan notifikasi khusus untuk warga
if (user?.role?.toLowerCase() == 'warga') ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.withOpacity(0.5)),
),
child: Row(
children: [
Icon(Icons.info_outline,
color: Colors.amber[800], size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
style: TextStyle(
fontSize: 14,
color: Colors.amber[900],
),
),
),
],
),
),
],
// Email
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
enabled: false, // Email tidak bisa diubah
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Nomor Telepon
TextField(
controller: controller.phoneController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
enabled: isEditing,
// Nama
TextField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.person),
enabled: canEdit,
),
),
const SizedBox(height: 16),
// Email
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.email),
enabled: false, // Email tidak bisa diubah
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
// Nomor Telepon
TextField(
controller: controller.phoneController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.phone),
enabled: canEdit,
),
keyboardType: TextInputType.phone,
),
],
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
// Informasi tambahan sesuai role
if (user != null) ...[
if (user.role?.toLowerCase() == 'warga') ...[
const SizedBox(height: 24),
const Text(
'Informasi Warga',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.infoColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
Icons.perm_identity, 'NIK', roleData?['nik'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(Icons.wc, 'Jenis Kelamin',
roleData?['jenis_kelamin'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(
Icons.home, 'Alamat', roleData?['alamat'] ?? '-'),
if (user.desa != null) ...[
const SizedBox(height: 8),
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
Row(
children: [
Icon(Icons.person_pin_circle,
color: AppTheme.infoColor),
const SizedBox(width: 10),
const Text(
'Informasi Lainnya',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.perm_identity, 'NIK',
roleData?['nik'] ?? '-'),
_buildInfoRow(Icons.wc, 'Jenis Kelamin',
roleData?['jenis_kelamin'] ?? '-'),
_buildInfoRow(
Icons.home, 'Alamat', roleData?['alamat'] ?? '-'),
if (user.desa != null) ...[
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
],
);
}),
],
);
}),
),
),
],
if (user.role?.toLowerCase() == 'donatur') ...[
const SizedBox(height: 24),
const Text(
'Informasi Donatur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.successColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.business, 'Instansi',
roleData?['instansi'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(
Icons.work, 'Jabatan', roleData?['jabatan'] ?? '-'),
Row(
children: [
Icon(Icons.volunteer_activism,
color: AppTheme.successColor),
const SizedBox(width: 10),
const Text(
'Informasi Donatur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.business, 'Instansi',
roleData?['instansi'] ?? '-'),
_buildInfoRow(Icons.work, 'Jabatan',
roleData?['jabatan'] ?? '-'),
],
);
}),
],
);
}),
),
),
],
if (user.role?.toLowerCase() == 'petugas_desa') ...[
const SizedBox(height: 24),
const Text(
'Informasi Petugas Desa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.primaryColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.badge, 'NIP', roleData?['nip'] ?? '-'),
if (user.desa != null) ...[
const SizedBox(height: 8),
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
Row(
children: [
Icon(Icons.badge, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Informasi Petugas Desa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
Icons.badge, 'NIP', roleData?['nip'] ?? '-'),
if (user.desa != null) ...[
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
],
);
}),
],
);
}),
),
),
],
],
],
@ -480,28 +630,54 @@ class ProfileView extends GetView<ProfileController> {
}
Widget _buildPasswordSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Keamanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showChangePasswordDialog(context),
icon: const Icon(Icons.lock),
label: const Text('Ubah Password'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.security, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Keamanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showChangePasswordDialog(context),
icon: const Icon(Icons.lock),
label: const Text('Ubah Password'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
],
),
);
}
@ -512,34 +688,52 @@ class ProfileView extends GetView<ProfileController> {
Get.dialog(
AlertDialog(
title: const Text('Ubah Password'),
title: Row(
children: [
Icon(Icons.lock_reset, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text('Ubah Password'),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: currentPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Password Saat Ini',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.key),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: newPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Password Baru',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.lock),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: confirmPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Konfirmasi Password Baru',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.lock_clock),
),
obscureText: true,
),
@ -550,6 +744,9 @@ class ProfileView extends GetView<ProfileController> {
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey[700],
),
),
ElevatedButton(
onPressed: () {
@ -561,6 +758,11 @@ class ProfileView extends GetView<ProfileController> {
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text('Simpan'),
),
@ -570,21 +772,65 @@ class ProfileView extends GetView<ProfileController> {
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: Colors.grey[700]),
const SizedBox(width: 8),
Expanded(
child: Text(
'$label: $value',
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 22,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value.isEmpty ? '-' : value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
],
),
);
}
}