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

@ -106,6 +106,16 @@ class WargaDashboardController extends GetxController {
super.onInit();
fetchData();
loadUserData();
// Atau gunakan timer untuk refresh data secara periodik
// Timer.periodic(Duration(seconds: 60), (_) => loadUserData());
}
@override
void onReady() {
super.onReady();
// Perbarui data user dan foto profil saat halaman siap
loadUserData();
}
void loadUserData() {
@ -133,7 +143,7 @@ class WargaDashboardController extends GetxController {
wargaData.fotoProfil != null &&
wargaData.fotoProfil!.isNotEmpty) {
fotoProfil.value = wargaData.fotoProfil!;
print('DEBUG WARGA: Foto profil: ${fotoProfil.value}');
print('DEBUG WARGA: Foto profil dari roleData: ${fotoProfil.value}');
}
} else {
print('DEBUG WARGA: User bukan warga');
@ -142,10 +152,8 @@ class WargaDashboardController extends GetxController {
print('DEBUG WARGA: userData null');
}
// Cek dan ambil foto profil jika belum ada
if (fotoProfil.isEmpty) {
_fetchProfilePhoto();
}
// Ambil foto profil dari database
_fetchProfilePhoto();
}
// Metode untuk mengambil foto profil
@ -156,12 +164,14 @@ class WargaDashboardController extends GetxController {
final wargaData = await _supabaseService.client
.from('warga')
.select('foto_profil')
.eq('user_id', user!.id)
.single();
.eq('id', user!.id) // Menggunakan id, bukan user_id
.maybeSingle();
if (wargaData != null && wargaData['foto_profil'] != null) {
fotoProfil.value = wargaData['foto_profil'];
print('DEBUG WARGA: Foto profil dari API: ${fotoProfil.value}');
} else {
print('DEBUG WARGA: Foto profil tidak ditemukan atau null');
}
} catch (e) {
print('Error fetching profile photo: $e');
@ -586,4 +596,13 @@ class WargaDashboardController extends GetxController {
isLoading.value = false;
}
}
// Metode untuk refresh data setelah update profil atau kembali ke halaman
Future<void> refreshData() async {
print('DEBUG WARGA: Memulai refresh data...');
await _authController.refreshUserData(); // Refresh data dari server
loadUserData(); // Muat ulang data ke variabel lokal
fetchData(); // Ambil data terkait lainnya
print('DEBUG WARGA: Refresh data selesai');
}
}

View File

@ -29,6 +29,8 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildStatisticSection(),
const SizedBox(height: 24),
_buildPenerimaanSummary(),
const SizedBox(height: 24),
_buildRecentPenerimaan(),
@ -296,6 +298,144 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
);
}
Widget _buildStatisticSection() {
// Data untuk statistik
final totalBantuan = controller.penerimaPenyaluran.length;
final totalDiterima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'DITERIMA')
.length;
final totalBelumMenerima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'BELUMMENERIMA')
.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Statistik Bantuan',
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatisticCard(
icon: Icons.check_circle,
color: Colors.green,
title: 'Diterima',
value: totalDiterima.toString(),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatisticCard(
icon: Icons.do_not_disturb,
color: Colors.red,
title: 'Belum Menerima',
value: totalBelumMenerima.toString(),
),
),
],
),
// Progress bar untuk persentase bantuan yang diterima
if (totalBantuan > 0) ...[
const SizedBox(height: 16),
Text(
'Kemajuan Penerimaan Bantuan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: totalDiterima / totalBantuan,
minHeight: 12,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
),
),
const SizedBox(height: 8),
Text(
'${(totalDiterima / totalBantuan * 100).toStringAsFixed(0)}% bantuan telah diterima',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
],
);
}
Widget _buildStatisticCard({
required IconData icon,
required Color color,
required String title,
required String value,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 20,
color: color,
),
),
const Spacer(),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
@ -324,49 +464,93 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
}
}
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [Colors.blue.shade50, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Ringkasan Bantuan',
titleStyle: const TextStyle(
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 16),
if (totalUang > 0)
_buildSummaryItem(
icon: Icons.attach_money,
color: Colors.green,
title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
if (totalNonUang.isNotEmpty) ...[
if (totalUang > 0) const SizedBox(height: 12),
...totalNonUang.entries.map((entry) {
return _buildSummaryItem(
icon: Icons.inventory_2,
color: Colors.blue,
title: 'Total Bantuan ${entry.key}',
value: '${entry.value} ${entry.key}',
);
}),
],
if (totalUang == 0 && totalNonUang.isEmpty)
_buildSummaryItem(
icon: Icons.info_outline,
color: Colors.grey,
title: 'Belum Ada Bantuan',
value: 'Anda belum menerima bantuan',
child: Column(
children: [
if (totalUang > 0)
_buildSummaryItem(
icon: Icons.attach_money,
color: Colors.green,
title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang),
),
if (totalNonUang.isNotEmpty) ...[
if (totalUang > 0)
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1),
),
...totalNonUang.entries.map((entry) {
return Column(
children: [
_buildSummaryItem(
icon: Icons.inventory_2,
color: Colors.blue,
title: 'Total Bantuan ${entry.key}',
value: '${entry.value} ${entry.key}',
),
if (entry != totalNonUang.entries.last)
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1),
),
],
);
}).toList(),
],
if (totalUang == 0 && totalNonUang.isEmpty)
_buildSummaryItem(
icon: Icons.info_outline,
color: Colors.grey,
title: 'Belum Ada Bantuan',
value: 'Anda belum menerima bantuan',
),
],
),
),
],
),
),
@ -422,53 +606,140 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
Widget _buildRecentPenerimaan() {
if (controller.penerimaPenyaluran.isEmpty) {
return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
SectionHeader(
title: 'Bantuan Terbaru',
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Icon(
Icons.info_outline,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum Ada Bantuan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
Text(
'Data bantuan akan muncul di sini ketika Anda menerima bantuan.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
final maxItems = controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed(Routes.wargaPenerimaan);
},
),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: maxItems,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: BantuanCard(
item: item,
isCompact: true,
onTap: () {
Get.toNamed('/warga/detail-penerimaan',
arguments: {'id': item.id});
},
),
);
},
),
if (controller.penerimaPenyaluran.length > 2)
Center(
child: TextButton.icon(
onPressed: () {
Get.toNamed('/warga-penerimaan');
},
icon: const Icon(Icons.list),
label: const Text('Lihat Semua Bantuan'),
),
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed(Routes.wargaPenerimaan);
},
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: maxItems,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: BantuanCard(
item: item,
isCompact: true,
onTap: () {
Get.toNamed('/warga/detail-penerimaan',
arguments: {'id': item.id});
},
),
);
},
),
if (controller.penerimaPenyaluran.length > 2)
Center(
child: ElevatedButton.icon(
onPressed: () {
Get.toNamed(Routes.wargaPenerimaan);
},
icon: const Icon(Icons.list),
label: const Text('Lihat Semua Bantuan'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
),
),
],
),
);
}
}

View File

@ -328,8 +328,8 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
),
const SizedBox(height: 8),
StatusBadge(
status:
penyaluran.statusPenerimaan ?? 'MENUNGGU',
status: penyaluran.statusPenerimaan ??
'BELUMMENERIMA',
fontSize: 14,
padding: const EdgeInsets.symmetric(
horizontal: 12,
@ -423,7 +423,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
),
const SizedBox(width: 8),
StatusBadge(
status: penyaluran.statusPenerimaan ?? 'MENUNGGU',
status: penyaluran.statusPenerimaan ?? 'BELUMMENERIMA',
fontSize: 12,
padding: const EdgeInsets.symmetric(
horizontal: 8,

View File

@ -4,16 +4,28 @@ import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_con
import 'package:penyaluran_app/app/modules/warga/views/warga_dashboard_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_penerimaan_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_pengaduan_view.dart';
import 'package:penyaluran_app/app/widgets/app_drawer.dart';
import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class WargaView extends GetView<WargaDashboardController> {
const WargaView({super.key});
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
WargaView({super.key});
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
// Tambahkan listener untuk refresh data saat fokus didapatkan kembali
// misalnya ketika kembali dari halaman profil
WidgetsBinding.instance.addPostFrameCallback((_) {
final focusNode = FocusNode();
FocusScope.of(context).requestFocus(focusNode);
focusNode.addListener(() {
if (focusNode.hasFocus) {
print('DEBUG WARGA: Halaman mendapatkan fokus, memuat ulang data');
controller.refreshData();
}
});
});
return Scaffold(
key: scaffoldKey,
@ -81,40 +93,7 @@ class WargaView extends GetView<WargaDashboardController> {
),
],
),
drawer: Obx(() => AppDrawer(
nama: controller.nama,
role: 'Warga',
desa: controller.desa,
notificationCount: controller.jumlahNotifikasiBelumDibaca.value,
onLogout: controller.logout,
menuItems: [
DrawerMenuItem(
icon: Icons.dashboard_outlined,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () => controller.changeTab(0),
),
DrawerMenuItem(
icon: Icons.volunteer_activism_outlined,
title: 'Penerimaan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () => controller.changeTab(1),
),
DrawerMenuItem(
icon: Icons.report_problem_outlined,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 2,
badgeCount: controller.totalPengaduanProses.value,
badgeColor: Colors.orange,
onTap: () => controller.changeTab(2),
),
DrawerMenuItem(
icon: Icons.description_outlined,
title: 'Laporan Penyaluran',
onTap: () => Get.toNamed('/laporan-penyaluran'),
),
],
)),
drawer: _buildDrawer(context),
body: Obx(() {
switch (controller.activeTabIndex.value) {
case 0:
@ -158,4 +137,276 @@ class WargaView extends GetView<WargaDashboardController> {
)),
);
}
Widget _buildDrawer(BuildContext context) {
// Muat ulang data foto profil ketika drawer dibuka
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.fotoProfil.isEmpty) {
controller.loadUserData();
}
});
return Drawer(
child: Column(
children: [
Container(
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 16,
bottom: 24,
left: 16,
right: 16),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty
? Icon(
Icons.person,
color: Colors.white,
size: 40,
)
: null,
),
),
SizedBox(height: 16),
Text(
'Halo,',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
Text(
controller.nama,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 22,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Warga',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on,
color: Colors.white,
size: 14,
),
SizedBox(width: 4),
Text(
controller.desa ?? 'Tidak ada desa',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
],
),
],
),
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildMenuCategory('Menu Utama'),
Obx(() => _buildMenuItem(
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () {
Navigator.pop(context);
controller.changeTab(0);
},
)),
Obx(() => _buildMenuItem(
icon: Icons.volunteer_activism_outlined,
activeIcon: Icons.volunteer_activism,
title: 'Penerimaan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () {
Navigator.pop(context);
controller.changeTab(1);
},
)),
Obx(() => _buildMenuItem(
icon: Icons.report_problem_outlined,
activeIcon: Icons.report_problem,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 2,
badge: controller.totalPengaduanProses.value > 0
? controller.totalPengaduanProses.value.toString()
: null,
onTap: () {
Navigator.pop(context);
controller.changeTab(2);
},
)),
_buildMenuCategory('Pengaturan'),
_buildMenuItem(
icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () async {
Navigator.pop(context);
await Get.toNamed('/profile');
// Refresh data ketika kembali dari profil
controller.refreshData();
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
onTap: () {
Navigator.pop(context);
controller.logout();
},
isLogout: true,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Widget _buildMenuCategory(String title) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
IconData? activeIcon,
required String title,
bool isSelected = false,
String? badge,
required Function() onTap,
bool isLogout = false,
}) {
return AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: ListTile(
leading: Icon(
isSelected ? (activeIcon ?? icon) : icon,
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
),
title: Text(
title,
style: TextStyle(
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
fontWeight: isSelected ? FontWeight.bold : null,
),
),
trailing: badge != null
? Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
badge,
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
)
: null,
onTap: onTap,
),
);
}
}