From 435435f9b66d26a0abb6f8a29ae5f1fad44a6fb6 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Thu, 13 Mar 2025 12:06:16 +0700 Subject: [PATCH] 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 --- .../bindings/donatur_binding.dart | 11 + .../controllers/donatur_controller.dart | 234 ++++++++ .../views/daftar_donatur_view.dart | 514 ++++++++++++++++++ .../petugas_desa/views/petugas_desa_view.dart | 8 + lib/app/routes/app_pages.dart | 7 + lib/app/routes/app_routes.dart | 2 + 6 files changed, 776 insertions(+) create mode 100644 lib/app/modules/petugas_desa/bindings/donatur_binding.dart create mode 100644 lib/app/modules/petugas_desa/controllers/donatur_controller.dart create mode 100644 lib/app/modules/petugas_desa/views/daftar_donatur_view.dart diff --git a/lib/app/modules/petugas_desa/bindings/donatur_binding.dart b/lib/app/modules/petugas_desa/bindings/donatur_binding.dart new file mode 100644 index 0000000..8ccb8c4 --- /dev/null +++ b/lib/app/modules/petugas_desa/bindings/donatur_binding.dart @@ -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(), + ); + } +} diff --git a/lib/app/modules/petugas_desa/controllers/donatur_controller.dart b/lib/app/modules/petugas_desa/controllers/donatur_controller.dart new file mode 100644 index 0000000..562b427 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/donatur_controller.dart @@ -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 daftarDonatur = [].obs; + final RxMap> penitipanPerDonatur = + >{}.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 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 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 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 getPenitipanBantuanByDonaturId( + String? donaturId) { + if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) { + return []; + } + + // Urutkan berdasarkan tanggal penitipan terbaru + final penitipanList = + List.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 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 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; + } +} diff --git a/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart new file mode 100644 index 0000000..bfd41c5 --- /dev/null +++ b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart @@ -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 { + 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 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, + ), + ); + }, + ); + } +} diff --git a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart index 716ddb5..638a4e2 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -297,6 +297,14 @@ class PetugasDesaView extends GetView { 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, diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 242adee..f6c1b7f 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -9,8 +9,10 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penerima_vi import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penitipan_view.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart'; import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart'; import 'package:penyaluran_app/app/modules/profile/views/profile_view.dart'; import 'package:penyaluran_app/app/modules/splash/bindings/splash_binding.dart'; @@ -74,5 +76,10 @@ class AppPages { page: () => const RiwayatPenitipanView(), binding: PetugasDesaBinding(), ), + GetPage( + name: _Paths.daftarDonatur, + page: () => const DaftarDonaturView(), + binding: DonaturBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index bff559d..2152064 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -17,6 +17,7 @@ abstract class Routes { static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran; static const profile = _Paths.profile; static const riwayatPenitipan = _Paths.riwayatPenitipan; + static const daftarDonatur = _Paths.daftarDonatur; } abstract class _Paths { @@ -36,4 +37,5 @@ abstract class _Paths { static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran'; static const profile = '/profile'; static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan'; + static const daftarDonatur = '/daftar-donatur'; }