Perbarui judul aplikasi dari 'Penyaluran App' menjadi 'Penerimaan App'. Tambahkan properti baru pada model PenerimaPenyaluranModel untuk mendukung informasi tambahan terkait penyaluran. Modifikasi tampilan di WargaDashboardView dan WargaPengaduanView untuk meningkatkan pengalaman pengguna. Hapus WargaPenyaluranView yang tidak digunakan dan perbarui rute aplikasi untuk mencerminkan perubahan ini.

This commit is contained in:
Khafidh Fuadi
2025-03-16 19:37:37 +07:00
parent a3798f0005
commit 76b167c65c
19 changed files with 1806 additions and 757 deletions

View File

@ -2,91 +2,147 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/bantuan_card.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
class WargaDashboardView extends GetView<WargaDashboardController> {
const WargaDashboardView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildPenyaluranSummary(),
const SizedBox(height: 24),
_buildPengajuanSection(),
const SizedBox(height: 24),
_buildPengaduanSummary(),
],
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildPenerimaanSummary(),
const SizedBox(height: 24),
_buildRecentPenerimaan(),
],
),
),
),
);
});
);
}),
);
}
Widget _buildWelcomeSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.blue.shade100,
child: const Icon(
Icons.person,
size: 40,
color: Colors.blue,
),
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.shade100,
child: Icon(
Icons.person,
color: Colors.blue.shade700,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang,',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
controller.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang,',
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.home,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Alamat tidak tersedia',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
color: Colors.grey.shade700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
controller.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.phone,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
'No. HP tidak tersedia',
style: TextStyle(
color: Colors.grey.shade700,
),
const SizedBox(height: 4),
Text(
controller.desa ?? 'Warga Desa',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.location_city,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
controller.desa ?? 'Desa tidak tersedia',
style: TextStyle(
color: Colors.grey.shade700,
),
],
),
),
],
),
],
),
@ -94,393 +150,179 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
);
}
Widget _buildPenyaluranSummary() {
Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Penyaluran Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Bantuan Diterima',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'${controller.totalPenyaluranDiterima.value}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const Icon(
Icons.volunteer_activism,
size: 40,
color: Colors.blue,
),
],
),
const Divider(height: 32),
if (controller.penerimaPenyaluran.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
item.keterangan ?? 'Bantuan',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPenerimaan!)
: '-',
),
trailing: Text(
item.jumlahBantuan != null
? currencyFormat.format(item.jumlahBantuan)
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada penyaluran bantuan'),
),
),
if (controller.penerimaPenyaluran.length > 2)
TextButton(
onPressed: () => controller.changeTab(1),
child: const Text('Lihat Semua'),
),
],
),
),
),
],
);
}
// Hitung total bantuan uang dan non-uang
double totalUang = 0;
Map<String, double> totalNonUang = {};
Widget _buildPengajuanSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengajuan Kelayakan Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatusCounter(
'Menunggu',
controller.totalPengajuanMenunggu.value,
Colors.orange,
),
_buildStatusCounter(
'Terverifikasi',
controller.totalPengajuanTerverifikasi.value,
Colors.green,
),
_buildStatusCounter(
'Ditolak',
controller.totalPengajuanDitolak.value,
Colors.red,
),
],
),
const Divider(height: 32),
if (controller.pengajuanKelayakan.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.pengajuanKelayakan.length > 3
? 3
: controller.pengajuanKelayakan.length,
itemBuilder: (context, index) {
final item = controller.pengajuanKelayakan[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Pengajuan #${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.createdAt != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.createdAt!)
: '-',
),
trailing: _buildStatusBadge(item.status),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada pengajuan kelayakan'),
),
),
if (controller.pengajuanKelayakan.length > 3)
TextButton(
onPressed: controller.goToPengajuanDetail,
child: const Text('Lihat Semua'),
),
],
),
),
),
],
);
}
Widget _buildStatusCounter(String label, int count, Color color) {
return Column(
children: [
Text(
'$count',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
);
}
Widget _buildStatusBadge(StatusKelayakan? status) {
if (status == null) return const SizedBox();
Color color;
String text;
switch (status) {
case StatusKelayakan.MENUNGGU:
color = Colors.orange;
text = 'Menunggu';
break;
case StatusKelayakan.TERVERIFIKASI:
color = Colors.green;
text = 'Terverifikasi';
break;
case StatusKelayakan.DITOLAK:
color = Colors.red;
text = 'Ditolak';
break;
for (var item in controller.penerimaPenyaluran) {
if (item.statusPenerimaan == 'DITERIMA') {
if (item.isUang == true && item.jumlahBantuan != null) {
totalUang += item.jumlahBantuan!;
} else if (item.isUang == false &&
item.jumlahBantuan != null &&
item.satuan != null) {
if (totalNonUang.containsKey(item.satuan)) {
totalNonUang[item.satuan!] =
(totalNonUang[item.satuan] ?? 0) + item.jumlahBantuan!;
} else {
totalNonUang[item.satuan!] = item.jumlahBantuan!;
}
}
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color),
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Ringkasan Bantuan',
titleStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
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 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',
),
],
),
),
);
}
Widget _buildPengaduanSummary() {
Widget _buildSummaryItem({
required IconData icon,
required Color color,
required String title,
required String value,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
),
],
);
}
Widget _buildRecentPenerimaan() {
if (controller.penerimaPenyaluran.isEmpty) {
return const SizedBox.shrink();
}
final maxItems = controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengaduan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed('/warga-penerimaan');
},
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatusCounter(
'Total',
controller.totalPengaduan.value,
Colors.blue,
),
_buildStatusCounter(
'Proses',
controller.totalPengaduanProses.value,
Colors.orange,
),
_buildStatusCounter(
'Selesai',
controller.totalPengaduanSelesai.value,
Colors.green,
),
],
),
const Divider(height: 32),
if (controller.pengaduan.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.pengaduan.length > 2
? 2
: controller.pengaduan.length,
itemBuilder: (context, index) {
final item = controller.pengaduan[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
item.judul ?? 'Pengaduan #${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPengaduan!)
: '-',
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: item.status == 'PROSES'
? Colors.orange.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: item.status == 'PROSES'
? Colors.orange
: Colors.green,
),
),
child: Text(
item.status == 'PROSES' ? 'Proses' : 'Selesai',
style: TextStyle(
color: item.status == 'PROSES'
? Colors.orange
: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada pengaduan'),
),
),
if (controller.pengaduan.length > 2)
TextButton(
onPressed: () => controller.changeTab(2),
child: const Text('Lihat Semua'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
// TODO: Implementasi navigasi ke halaman buat pengaduan
},
icon: const Icon(Icons.add),
label: const Text('Buat Pengaduan Baru'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
),
],
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: TextButton.icon(
onPressed: () {
Get.toNamed('/warga-penerimaan');
},
icon: const Icon(Icons.list),
label: const Text('Lihat Semua Bantuan'),
),
),
),
],
);
}