Tambahkan rute dan tampilan untuk daftar donatur

- Tambahkan tampilan DaftarDonaturView dan binding DonaturBinding
- Perbarui AppPages untuk menambahkan rute ke daftar donatur
- Tambahkan item menu 'Daftar Donatur' di PetugasDesaView untuk navigasi
This commit is contained in:
Khafidh Fuadi
2025-03-13 12:06:16 +07:00
parent d8bf361d21
commit 435435f9b6
6 changed files with 776 additions and 0 deletions

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/donatur_controller.dart';
class DonaturBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<DonaturController>(
() => DonaturController(),
);
}
}

View File

@ -0,0 +1,234 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
class DonaturController extends GetxController {
final RxList<DonaturModel> daftarDonatur = <DonaturModel>[].obs;
final RxMap<String, List<PenitipanBantuanModel>> penitipanPerDonatur =
<String, List<PenitipanBantuanModel>>{}.obs;
final RxBool isLoading = false.obs;
final SupabaseService _supabaseService = SupabaseService.to;
@override
void onInit() {
super.onInit();
fetchDaftarDonatur();
}
@override
void onReady() {
super.onReady();
// Pastikan data dimuat saat controller siap
if (daftarDonatur.isEmpty) {
fetchDaftarDonatur();
}
}
Future<void> fetchDaftarDonatur() async {
isLoading.value = true;
try {
final result = await _supabaseService.getDaftarDonatur();
if (result != null) {
// Konversi data ke model Donatur
daftarDonatur.value =
result.map((data) => DonaturModel.fromJson(data)).toList();
// Ambil data penitipan bantuan
await fetchPenitipanBantuan();
} else {
// Jika result null, tampilkan daftar kosong
daftarDonatur.value = [];
}
} catch (e) {
print('Error saat mengambil data donatur: $e');
// Tampilkan pesan error jika diperlukan
} finally {
isLoading.value = false;
}
}
Future<void> fetchPenitipanBantuan() async {
try {
final result = await _supabaseService.getPenitipanBantuan();
if (result != null) {
// Reset map penitipan per donatur
penitipanPerDonatur.clear();
// Konversi data ke model PenitipanBantuan dan kelompokkan berdasarkan donatur_id
for (var data in result) {
final penitipan = PenitipanBantuanModel.fromJson(data);
if (penitipan.donaturId != null) {
if (!penitipanPerDonatur.containsKey(penitipan.donaturId)) {
penitipanPerDonatur[penitipan.donaturId!] = [];
}
penitipanPerDonatur[penitipan.donaturId]!.add(penitipan);
}
}
}
} catch (e) {
print('Error saat mengambil data penitipan bantuan: $e');
}
}
// Mendapatkan jumlah donasi untuk donatur tertentu
int getJumlahDonasi(String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return 0;
}
return penitipanPerDonatur[donaturId]!.length;
}
// Mendapatkan total nilai donasi untuk donatur tertentu
double getTotalNilaiDonasi(String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return 0;
}
double total = 0;
for (var penitipan in penitipanPerDonatur[donaturId]!) {
if (penitipan.jumlah != null) {
// Untuk donasi uang, kita gunakan nilai jumlah langsung
// Untuk donasi barang, kita perlu implementasi lain jika ada nilai barang
if (penitipan.isUang == true) {
total += penitipan.jumlah!;
}
// Jika ingin menambahkan nilai barang, tambahkan logika di sini
}
}
return total;
}
// Mendapatkan total nilai donasi uang untuk donatur tertentu
double getTotalNilaiDonasiUang(String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return 0;
}
double total = 0;
for (var penitipan in penitipanPerDonatur[donaturId]!) {
if (penitipan.jumlah != null && penitipan.isUang == true) {
total += penitipan.jumlah!;
}
}
return total;
}
// Mendapatkan jumlah donasi uang untuk donatur tertentu
int getJumlahDonasiUang(String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return 0;
}
return penitipanPerDonatur[donaturId]!
.where((penitipan) => penitipan.isUang == true)
.length;
}
// Mendapatkan jumlah donasi barang untuk donatur tertentu
int getJumlahDonasiBarang(String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return 0;
}
return penitipanPerDonatur[donaturId]!
.where((penitipan) => penitipan.isUang != true)
.length;
}
// Format nilai donasi ke format Rupiah
String formatRupiah(double nominal) {
return 'Rp ${nominal.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
}
DonaturModel? getDonaturById(String id) {
try {
if (daftarDonatur.isEmpty) {
// Jika data belum dimuat, muat data terlebih dahulu
fetchDaftarDonatur();
return null;
}
return daftarDonatur.firstWhere((donatur) => donatur.id == id);
} catch (e) {
return null;
}
}
// Fungsi untuk mengambil data donatur langsung dari database
Future<DonaturModel?> fetchDonaturById(String id) async {
try {
final data = await _supabaseService.getDonaturById(id);
if (data != null) {
return DonaturModel.fromJson(data);
}
return null;
} catch (e) {
print('Error saat mengambil data donatur by ID: $e');
return null;
}
}
// Mendapatkan daftar penitipan bantuan untuk donatur tertentu
List<PenitipanBantuanModel> getPenitipanBantuanByDonaturId(
String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return [];
}
// Urutkan berdasarkan tanggal penitipan terbaru
final penitipanList =
List<PenitipanBantuanModel>.from(penitipanPerDonatur[donaturId]!);
penitipanList.sort((a, b) {
if (a.tanggalPenitipan == null) return 1;
if (b.tanggalPenitipan == null) return -1;
return b.tanggalPenitipan!.compareTo(a.tanggalPenitipan!);
});
return penitipanList;
}
// Mendapatkan daftar penitipan bantuan uang untuk donatur tertentu
List<PenitipanBantuanModel> getPenitipanBantuanUangByDonaturId(
String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return [];
}
// Filter penitipan uang dan urutkan berdasarkan tanggal penitipan terbaru
final penitipanList = penitipanPerDonatur[donaturId]!
.where((penitipan) => penitipan.isUang == true)
.toList();
penitipanList.sort((a, b) {
if (a.tanggalPenitipan == null) return 1;
if (b.tanggalPenitipan == null) return -1;
return b.tanggalPenitipan!.compareTo(a.tanggalPenitipan!);
});
return penitipanList;
}
// Mendapatkan daftar penitipan bantuan barang untuk donatur tertentu
List<PenitipanBantuanModel> getPenitipanBantuanBarangByDonaturId(
String? donaturId) {
if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) {
return [];
}
// Filter penitipan barang dan urutkan berdasarkan tanggal penitipan terbaru
final penitipanList = penitipanPerDonatur[donaturId]!
.where((penitipan) => penitipan.isUang != true)
.toList();
penitipanList.sort((a, b) {
if (a.tanggalPenitipan == null) return 1;
if (b.tanggalPenitipan == null) return -1;
return b.tanggalPenitipan!.compareTo(a.tanggalPenitipan!);
});
return penitipanList;
}
}

View File

@ -0,0 +1,514 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/donatur_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
class DaftarDonaturView extends GetView<DonaturController> {
const DaftarDonaturView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Daftar Donatur'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// Implementasi pencarian
showSearch(
context: context,
delegate: DonaturSearchDelegate(controller),
);
},
),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (controller.daftarDonatur.isEmpty) {
return const Center(
child: Text('Tidak ada data donatur'),
);
}
return SingleChildScrollView(
child: Column(
children: [
_buildDonaturSummary(context),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: controller.daftarDonatur.length,
itemBuilder: (context, index) {
final donatur = controller.daftarDonatur[index];
return _buildDonaturCard(context, donatur);
},
),
],
),
);
}),
);
}
// Ringkasan daftar donatur
Widget _buildDonaturSummary(BuildContext context) {
// Hitung jumlah donatur berdasarkan jenis
final jumlahPerusahaan =
controller.daftarDonatur.where((d) => d.jenis == 'Perusahaan').length;
final jumlahOrganisasi =
controller.daftarDonatur.where((d) => d.jenis == 'Organisasi').length;
final jumlahIndividu =
controller.daftarDonatur.where((d) => d.jenis == 'Individu').length;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Donatur',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.business,
title: 'Perusahaan',
value: '$jumlahPerusahaan',
color: Colors.blue,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.groups,
title: 'Organisasi',
value: '$jumlahOrganisasi',
color: Colors.green,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.person,
title: 'Individu',
value: '$jumlahIndividu',
color: Colors.orange,
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required Color color,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildDonaturCard(BuildContext context, DonaturModel donatur) {
// Pilih ikon berdasarkan jenis donatur
IconData jenisIcon;
switch (donatur.jenis) {
case 'Perusahaan':
jenisIcon = Icons.business;
break;
case 'Organisasi':
jenisIcon = Icons.groups;
break;
case 'Individu':
jenisIcon = Icons.person;
break;
default:
jenisIcon = Icons.help_outline;
}
// Hitung jumlah donasi dan total nilai donasi
final jumlahDonasi = controller.getJumlahDonasi(donatur.id);
final jumlahDonasiUang = controller.getJumlahDonasiUang(donatur.id);
final jumlahDonasiBarang = controller.getJumlahDonasiBarang(donatur.id);
final totalNilaiDonasiUang = controller.getTotalNilaiDonasiUang(donatur.id);
final totalNilaiDonasiUangFormatted =
controller.formatRupiah(totalNilaiDonasiUang);
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () {
// Navigasi ke halaman detail donatur (akan diimplementasikan nanti)
// Get.toNamed('/daftar-donatur/detail', arguments: donatur.id);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Foto profil atau ikon
CircleAvatar(
radius: 30,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: Icon(
jenisIcon,
size: 30,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 16),
// Informasi donatur
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
donatur.nama ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
_buildStatusBadge(donatur.status),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
jenisIcon,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
donatur.jenis ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
// Informasi donasi
Row(
children: [
Expanded(
child: Row(
children: [
const Icon(
Icons.attach_money,
size: 14,
color: Color.fromARGB(255, 210, 158, 4),
),
const SizedBox(width: 4),
Text(
'${jumlahDonasiUang}x Donasi Uang',
style: const TextStyle(
fontSize: 12,
color: Color.fromARGB(255, 210, 158, 4),
),
),
],
),
),
Expanded(
child: Row(
children: [
const Icon(
Icons.inventory_2,
size: 14,
color: Colors.purple,
),
const SizedBox(width: 4),
Text(
'${jumlahDonasiBarang}x Donasi Barang',
style: const TextStyle(
fontSize: 12,
color: Colors.purple,
),
),
],
),
),
],
),
],
),
),
],
),
),
),
);
}
Widget _buildStatusBadge(String? status) {
Color backgroundColor;
Color textColor;
String label;
switch (status) {
case 'AKTIF':
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green;
label = 'Aktif';
break;
case 'NONAKTIF':
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red;
label = 'Non Aktif';
break;
default:
backgroundColor = Colors.grey.withOpacity(0.1);
textColor = Colors.grey;
label = 'Tidak diketahui';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: textColor,
),
),
);
}
}
class DonaturSearchDelegate extends SearchDelegate {
final DonaturController controller;
DonaturSearchDelegate(this.controller);
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
close(context, null);
},
);
}
@override
Widget buildResults(BuildContext context) {
return _buildSearchResults();
}
@override
Widget buildSuggestions(BuildContext context) {
return _buildSearchResults();
}
Widget _buildSearchResults() {
final filteredList = controller.daftarDonatur.where((donatur) {
final nama = donatur.nama?.toLowerCase() ?? '';
final jenis = donatur.jenis?.toLowerCase() ?? '';
final alamat = donatur.alamat?.toLowerCase() ?? '';
final searchLower = query.toLowerCase();
return nama.contains(searchLower) ||
jenis.contains(searchLower) ||
alamat.contains(searchLower);
}).toList();
if (filteredList.isEmpty) {
return const Center(
child: Text('Tidak ada hasil yang ditemukan'),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final donatur = filteredList[index];
// Pilih ikon berdasarkan jenis donatur
IconData jenisIcon;
switch (donatur.jenis) {
case 'Perusahaan':
jenisIcon = Icons.business;
break;
case 'Organisasi':
jenisIcon = Icons.groups;
break;
case 'Individu':
jenisIcon = Icons.person;
break;
default:
jenisIcon = Icons.help_outline;
}
// Hitung jumlah donasi dan total nilai donasi
final jumlahDonasi = controller.getJumlahDonasi(donatur.id);
final jumlahDonasiUang = controller.getJumlahDonasiUang(donatur.id);
final jumlahDonasiBarang = controller.getJumlahDonasiBarang(donatur.id);
final totalNilaiDonasiUang =
controller.getTotalNilaiDonasiUang(donatur.id);
final totalNilaiDonasiUangFormatted =
controller.formatRupiah(totalNilaiDonasiUang);
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
onTap: () {
close(context, null);
// Get.toNamed('/daftar-donatur/detail', arguments: donatur.id);
},
leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: Icon(
jenisIcon,
color: AppTheme.primaryColor,
),
),
title: Text(
donatur.nama ?? '',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(donatur.jenis ?? ''),
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icons.monetization_on,
size: 12,
color: Colors.green,
),
const SizedBox(width: 4),
Text(
totalNilaiDonasiUangFormatted,
style: const TextStyle(
fontSize: 12,
color: Colors.green,
),
),
const SizedBox(width: 8),
const Icon(
Icons.volunteer_activism,
size: 12,
color: Colors.blue,
),
const SizedBox(width: 4),
Text(
'$jumlahDonasi Donasi',
style: const TextStyle(
fontSize: 12,
color: Colors.blue,
),
),
],
),
],
),
isThreeLine: true,
trailing: donatur.status == 'AKTIF'
? const Icon(
Icons.verified,
color: Colors.green,
)
: null,
),
);
},
);
}
}

View File

@ -297,6 +297,14 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Get.toNamed('/daftar-penerima');
},
),
ListTile(
leading: const Icon(Icons.volunteer_activism),
title: const Text('Daftar Donatur'),
onTap: () {
Navigator.pop(context); // Tutup drawer terlebih dahulu
Get.toNamed('/daftar-donatur');
},
),
ListTile(
leading: Stack(
alignment: Alignment.center,