From d9cc7aaf92252690b0cb0577a5add0fad279a600 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Thu, 13 Mar 2025 13:28:09 +0700 Subject: [PATCH] Perbarui DonaturController dan tampilan terkait untuk mendukung stok bantuan - Tambahkan metode fetchStokBantuan di DonaturController untuk mengambil data stok bantuan - Perbarui tampilan DaftarDonaturView untuk navigasi ke detail donatur - Tambahkan rute dan tampilan detail donatur - Ganti beberapa warna ikon dan teks untuk konsistensi tampilan - Gunakan dialog DetailPenitipanDialog untuk menampilkan detail penitipan --- .../controllers/donatur_controller.dart | 58 ++ .../views/daftar_donatur_view.dart | 19 +- .../views/detail_donatur_view.dart | 553 ++++++++++++++++++ .../petugas_desa/views/penitipan_view.dart | 205 +------ .../petugas_desa/views/petugas_desa_view.dart | 2 +- .../views/riwayat_penitipan_view.dart | 2 - lib/app/routes/app_pages.dart | 6 + lib/app/routes/app_routes.dart | 2 + lib/app/widgets/detail_penitipan_dialog.dart | 306 ++++++++++ 9 files changed, 949 insertions(+), 204 deletions(-) create mode 100644 lib/app/modules/petugas_desa/views/detail_donatur_view.dart create mode 100644 lib/app/widgets/detail_penitipan_dialog.dart diff --git a/lib/app/modules/petugas_desa/controllers/donatur_controller.dart b/lib/app/modules/petugas_desa/controllers/donatur_controller.dart index 562b427..6fa1327 100644 --- a/lib/app/modules/petugas_desa/controllers/donatur_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/donatur_controller.dart @@ -2,11 +2,14 @@ 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'; +import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; class DonaturController extends GetxController { final RxList daftarDonatur = [].obs; final RxMap> penitipanPerDonatur = >{}.obs; + final RxMap stokBantuanMap = + {}.obs; final RxBool isLoading = false.obs; final SupabaseService _supabaseService = SupabaseService.to; @@ -14,6 +17,7 @@ class DonaturController extends GetxController { void onInit() { super.onInit(); fetchDaftarDonatur(); + fetchStokBantuan(); } @override @@ -74,6 +78,25 @@ class DonaturController extends GetxController { } } + Future fetchStokBantuan() async { + try { + final result = await _supabaseService.getStokBantuan(); + + if (result != null) { + stokBantuanMap.clear(); + + for (var data in result) { + final stokBantuan = StokBantuanModel.fromJson(data); + if (stokBantuan.id != null) { + stokBantuanMap[stokBantuan.id!] = stokBantuan; + } + } + } + } catch (e) { + print('Error saat mengambil data stok bantuan: $e'); + } + } + // Mendapatkan jumlah donasi untuk donatur tertentu int getJumlahDonasi(String? donaturId) { if (donaturId == null || !penitipanPerDonatur.containsKey(donaturId)) { @@ -231,4 +254,39 @@ class DonaturController extends GetxController { return penitipanList; } + + String getStokBantuanSatuan(String? stokBantuanId) { + if (stokBantuanId == null || !stokBantuanMap.containsKey(stokBantuanId)) { + return 'item'; + } + return stokBantuanMap[stokBantuanId]?.satuan ?? 'item'; + } + + String getStokBantuanNama(String? stokBantuanId) { + if (stokBantuanId == null || !stokBantuanMap.containsKey(stokBantuanId)) { + return ''; + } + return stokBantuanMap[stokBantuanId]?.nama ?? ''; + } + + // Mendapatkan nama donatur berdasarkan ID + String? getDonaturNama(String? donaturId) { + if (donaturId == null) return null; + + try { + final donatur = daftarDonatur.firstWhere((d) => d.id == donaturId); + return donatur.nama; + } catch (e) { + return null; + } + } + + // Mendapatkan nama petugas desa berdasarkan ID + String? getPetugasDesaNama(String? petugasDesaId) { + if (petugasDesaId == null) return null; + + // Implementasi ini perlu disesuaikan dengan cara aplikasi menyimpan data petugas desa + // Contoh sederhana: + return 'Petugas Desa'; // Ganti dengan implementasi yang sesuai + } } diff --git a/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart index bfd41c5..c1e7fa2 100644 --- a/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart +++ b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart @@ -3,6 +3,7 @@ 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'; +import 'package:penyaluran_app/app/routes/app_pages.dart'; class DaftarDonaturView extends GetView { const DaftarDonaturView({super.key}); @@ -199,8 +200,8 @@ class DaftarDonaturView extends GetView { ), child: InkWell( onTap: () { - // Navigasi ke halaman detail donatur (akan diimplementasikan nanti) - // Get.toNamed('/daftar-donatur/detail', arguments: donatur.id); + // Navigasi ke halaman detail donatur + Get.toNamed(Routes.detailDonatur, arguments: donatur.id); }, borderRadius: BorderRadius.circular(12), child: Padding( @@ -268,14 +269,14 @@ class DaftarDonaturView extends GetView { const Icon( Icons.attach_money, size: 14, - color: Color.fromARGB(255, 210, 158, 4), + color: Colors.green, ), const SizedBox(width: 4), Text( '${jumlahDonasiUang}x Donasi Uang', style: const TextStyle( fontSize: 12, - color: Color.fromARGB(255, 210, 158, 4), + color: Colors.green, ), ), ], @@ -287,14 +288,14 @@ class DaftarDonaturView extends GetView { const Icon( Icons.inventory_2, size: 14, - color: Colors.purple, + color: Colors.orange, ), const SizedBox(width: 4), Text( '${jumlahDonasiBarang}x Donasi Barang', style: const TextStyle( fontSize: 12, - color: Colors.purple, + color: Colors.orange, ), ), ], @@ -319,8 +320,8 @@ class DaftarDonaturView extends GetView { switch (status) { case 'AKTIF': - backgroundColor = Colors.green.withOpacity(0.1); - textColor = Colors.green; + backgroundColor = Colors.blue.withOpacity(0.1); + textColor = Colors.blue; label = 'Aktif'; break; case 'NONAKTIF': @@ -446,7 +447,7 @@ class DonaturSearchDelegate extends SearchDelegate { child: ListTile( onTap: () { close(context, null); - // Get.toNamed('/daftar-donatur/detail', arguments: donatur.id); + Get.toNamed(Routes.detailDonatur, arguments: donatur.id); }, leading: CircleAvatar( backgroundColor: AppTheme.primaryColor.withOpacity(0.1), diff --git a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart new file mode 100644 index 0000000..c556b38 --- /dev/null +++ b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart @@ -0,0 +1,553 @@ +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'; +import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; +import 'package:penyaluran_app/app/widgets/detail_penitipan_dialog.dart'; +import 'package:intl/intl.dart'; + +class DetailDonaturView extends GetView { + const DetailDonaturView({super.key}); + + @override + Widget build(BuildContext context) { + final String donaturId = Get.arguments as String; + + return Scaffold( + appBar: AppBar( + title: const Text('Detail Donatur'), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // Implementasi edit donatur (akan diimplementasikan nanti) + }, + ), + ], + ), + body: FutureBuilder( + future: controller.fetchDonaturById(donaturId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } + + if (!snapshot.hasData || snapshot.data == null) { + return const Center( + child: Text('Data donatur tidak ditemukan'), + ); + } + + final donatur = snapshot.data!; + return _buildDetailContent(context, donatur); + }, + ), + ); + } + + Widget _buildDetailContent(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 SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan informasi utama donatur + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Avatar dan nama donatur + Row( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: Icon( + jenisIcon, + size: 40, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + donatur.nama ?? 'Tanpa Nama', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: donatur.jenis == 'Perusahaan' + ? Colors.blue.withOpacity(0.1) + : donatur.jenis == 'Organisasi' + ? Colors.green.withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + donatur.jenis ?? 'Tidak Diketahui', + style: TextStyle( + fontSize: 12, + color: donatur.jenis == 'Perusahaan' + ? Colors.blue + : donatur.jenis == 'Organisasi' + ? Colors.green + : Colors.orange, + ), + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: donatur.status == 'AKTIF' + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + donatur.status == 'AKTIF' + ? Icons.check_circle + : Icons.cancel, + size: 12, + color: donatur.status == 'AKTIF' + ? Colors.green + : Colors.red, + ), + const SizedBox(width: 4), + Text( + donatur.status ?? 'TIDAK AKTIF', + style: TextStyle( + fontSize: 12, + color: donatur.status == 'AKTIF' + ? Colors.green + : Colors.red, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + // Informasi kontak + const Divider(), + const SizedBox(height: 8), + _buildInfoItem(Icons.location_on, 'Alamat', + donatur.alamat ?? 'Tidak ada alamat'), + const SizedBox(height: 8), + _buildInfoItem(Icons.phone, 'Telepon', + donatur.telepon ?? 'Tidak ada telepon'), + const SizedBox(height: 8), + _buildInfoItem( + Icons.email, 'Email', donatur.email ?? 'Tidak ada email'), + const SizedBox(height: 8), + _buildInfoItem( + Icons.calendar_today, + 'Terdaftar Sejak', + donatur.createdAt != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(donatur.createdAt!) + : 'Tidak diketahui', + ), + ], + ), + ), + ), + const SizedBox(height: 16), + // Ringkasan donasi + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ringkasan Donasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'Total Donasi', + '$jumlahDonasi', + Icons.volunteer_activism, + Colors.blue, + ), + ), + Expanded( + child: _buildStatItem( + 'Donasi Uang', + '$jumlahDonasiUang', + Icons.monetization_on, + Colors.green, + ), + ), + Expanded( + child: _buildStatItem( + 'Donasi Barang', + '$jumlahDonasiBarang', + Icons.inventory_2, + Colors.orange, + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Row( + children: [ + const Icon( + Icons.monetization_on, + color: Colors.green, + ), + const SizedBox(width: 8), + const Text( + 'Total Nilai Donasi Uang:', + style: TextStyle( + fontSize: 16, + ), + ), + const Spacer(), + Text( + totalNilaiDonasiUangFormatted, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 16), + // Riwayat donasi + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Riwayat Donasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + TextButton( + onPressed: () { + // Navigasi ke halaman riwayat donasi lengkap + }, + child: const Text('Lihat Semua'), + ), + ], + ), + const SizedBox(height: 8), + _buildRiwayatDonasi(donatur.id), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildInfoItem(IconData icon, String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 20, + color: Colors.grey[600], + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildStatItem( + String label, String value, IconData icon, Color color) { + return Column( + children: [ + CircleAvatar( + radius: 25, + backgroundColor: color.withOpacity(0.1), + child: Icon( + icon, + color: color, + ), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildRiwayatDonasi(String? donaturId) { + if (donaturId == null || + !controller.penitipanPerDonatur.containsKey(donaturId)) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: Text('Belum ada riwayat donasi'), + ), + ); + } + + final penitipanList = controller.penitipanPerDonatur[donaturId]!; + + // Tampilkan maksimal 3 donasi terbaru + final displayedPenitipan = + penitipanList.length > 3 ? penitipanList.sublist(0, 3) : penitipanList; + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: displayedPenitipan.length, + itemBuilder: (context, index) { + final penitipan = displayedPenitipan[index]; + return _buildDonasiItem(penitipan); + }, + ); + } + + Widget _buildDonasiItem(PenitipanBantuanModel penitipan) { + final isUang = penitipan.isUang == true; + final tanggal = penitipan.createdAt != null + ? DateFormat('dd MMM yyyy', 'id_ID').format(penitipan.createdAt!) + : 'Tanggal tidak diketahui'; + + String nilaiDonasi = ''; + if (isUang && penitipan.jumlah != null) { + nilaiDonasi = controller.formatRupiah(penitipan.jumlah!); + } else if (penitipan.jumlah != null) { + final satuan = controller.getStokBantuanSatuan(penitipan.stokBantuanId); + nilaiDonasi = '${penitipan.jumlah} $satuan'; + } else { + nilaiDonasi = 'Jumlah tidak diketahui'; + } + + return Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 0, + color: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + onTap: () { + // Tampilkan dialog detail penitipan + _showDetailPenitipan(penitipan); + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: isUang + ? Colors.green.withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + child: Icon( + isUang ? Icons.monetization_on : Icons.inventory_2, + color: isUang ? Colors.green : Colors.orange, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isUang + ? 'Donasi Uang' + : controller + .getStokBantuanNama(penitipan.stokBantuanId) + .isNotEmpty + ? controller + .getStokBantuanNama(penitipan.stokBantuanId) + : penitipan.deskripsi ?? 'Donasi Barang', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + tanggal, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + Text( + nilaiDonasi, + style: TextStyle( + fontWeight: FontWeight.bold, + color: isUang ? Colors.green : Colors.orange, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.arrow_forward_ios, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ), + ), + ); + } + + // Metode untuk menampilkan dialog detail penitipan + void _showDetailPenitipan(PenitipanBantuanModel penitipan) { + // Dapatkan data yang diperlukan + final donaturNama = penitipan.donatur?.nama ?? + controller.getDonaturNama(penitipan.donaturId) ?? + 'Donatur tidak ditemukan'; + + final kategoriNama = penitipan.kategoriBantuan?.nama ?? + controller.getStokBantuanNama(penitipan.stokBantuanId); + + final kategoriSatuan = penitipan.kategoriBantuan?.satuan ?? + controller.getStokBantuanSatuan(penitipan.stokBantuanId); + + // Tampilkan dialog + DetailPenitipanDialog.show( + context: Get.context!, + item: penitipan, + donaturNama: donaturNama, + kategoriNama: kategoriNama, + kategoriSatuan: kategoriSatuan, + getPetugasDesaNama: (String? id) => + controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui', + showFullScreenImage: (String imageUrl) { + DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl); + }, + ); + } +} diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index 5d8bd57..5897de9 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/date_formatter.dart'; +import 'package:penyaluran_app/app/widgets/detail_penitipan_dialog.dart'; import 'dart:io'; class PenitipanView extends GetView { @@ -694,198 +695,18 @@ class PenitipanView extends GetView { final kategoriSatuan = item.kategoriBantuan?.satuan ?? controller.getKategoriSatuan(item.stokBantuanId); - // Cek apakah penitipan berbentuk uang - final isUang = item.isUang ?? false; - - Get.dialog( - AlertDialog( - title: const Text('Detail Penitipan'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailItem('Donatur', donaturNama), - _buildDetailItem('Status', item.status ?? 'Tidak diketahui'), - _buildDetailItem('Kategori Bantuan', kategoriNama), - _buildDetailItem( - 'Jumlah', - isUang - ? 'Rp ${DateFormatter.formatNumber(item.jumlah)}' - : '${DateFormatter.formatNumber(item.jumlah)} $kategoriSatuan'), - if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'), - _buildDetailItem( - 'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'), - _buildDetailItem( - 'Tanggal Penitipan', - DateFormatter.formatDateTime(item.tanggalPenitipan, - defaultValue: 'Tidak ada tanggal'), - ), - if (item.tanggalVerifikasi != null) - _buildDetailItem( - 'Tanggal Verifikasi', - DateFormatter.formatDateTime(item.tanggalVerifikasi), - ), - if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) - _buildDetailItem( - 'Diverifikasi Oleh', - controller.getPetugasDesaNama(item.petugasDesaId), - ), - _buildDetailItem('Tanggal Dibuat', - DateFormatter.formatDateTime(item.createdAt)), - if (item.alasanPenolakan != null && - item.alasanPenolakan!.isNotEmpty) - _buildDetailItem('Alasan Penolakan', item.alasanPenolakan!), - - // Foto Bantuan - if (!isUang && - item.fotoBantuan != null && - item.fotoBantuan!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - const Text( - 'Foto Bantuan:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - SizedBox( - height: 100, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: item.fotoBantuan!.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - _showFullScreenImage( - context, item.fotoBantuan![index]); - }, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - item.fotoBantuan![index], - height: 100, - width: 100, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 100, - width: 100, - color: Colors.grey.shade300, - child: const Icon(Icons.error), - ); - }, - ), - ), - ), - ); - }, - ), - ), - ], - ), - - // Bukti Transfer (untuk bantuan uang) - if (isUang && - item.fotoBantuan != null && - item.fotoBantuan!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - const Text( - 'Bukti Transfer:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - SizedBox( - height: 100, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: item.fotoBantuan!.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - _showFullScreenImage( - context, item.fotoBantuan![index]); - }, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - item.fotoBantuan![index], - height: 100, - width: 100, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 100, - width: 100, - color: Colors.grey.shade300, - child: const Icon(Icons.error), - ); - }, - ), - ), - ), - ); - }, - ), - ), - ], - ), - - // Bukti Serah Terima - if (item.fotoBuktiSerahTerima != null && - item.fotoBuktiSerahTerima!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - const Text( - 'Bukti Serah Terima:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - GestureDetector( - onTap: () { - _showFullScreenImage( - context, item.fotoBuktiSerahTerima!); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - item.fotoBuktiSerahTerima!, - height: 200, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 200, - width: double.infinity, - color: Colors.grey.shade300, - child: const Icon(Icons.error), - ); - }, - ), - ), - ), - ], - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Tutup'), - ), - ], - ), + // Gunakan dialog yang sudah dibuat + DetailPenitipanDialog.show( + context: context, + item: item, + donaturNama: donaturNama, + kategoriNama: kategoriNama, + kategoriSatuan: kategoriSatuan, + getPetugasDesaNama: (String? id) => + controller.getPetugasDesaNama(id) ?? 'Tidak diketahui', + showFullScreenImage: (String imageUrl) { + DetailPenitipanDialog.showFullScreenImage(context, imageUrl); + }, ); } 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 638a4e2..51fbfbc 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -298,7 +298,7 @@ class PetugasDesaView extends GetView { }, ), ListTile( - leading: const Icon(Icons.volunteer_activism), + leading: const Icon(Icons.volunteer_activism_outlined), title: const Text('Daftar Donatur'), onTap: () { Navigator.pop(context); // Tutup drawer terlebih dahulu diff --git a/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart b/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart index 3cf9b40..62e6ea2 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; -import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/date_formatter.dart'; -import 'dart:io'; class RiwayatPenitipanView extends GetView { const RiwayatPenitipanView({super.key}); diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index f6c1b7f..026c911 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -10,6 +10,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerim 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/views/detail_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'; @@ -81,5 +82,10 @@ class AppPages { page: () => const DaftarDonaturView(), binding: DonaturBinding(), ), + GetPage( + name: _Paths.detailDonatur, + page: () => const DetailDonaturView(), + binding: DonaturBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 2152064..5b45108 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -18,6 +18,7 @@ abstract class Routes { static const profile = _Paths.profile; static const riwayatPenitipan = _Paths.riwayatPenitipan; static const daftarDonatur = _Paths.daftarDonatur; + static const detailDonatur = _Paths.detailDonatur; } abstract class _Paths { @@ -38,4 +39,5 @@ abstract class _Paths { static const profile = '/profile'; static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan'; static const daftarDonatur = '/daftar-donatur'; + static const detailDonatur = '/daftar-donatur/detail'; } diff --git a/lib/app/widgets/detail_penitipan_dialog.dart b/lib/app/widgets/detail_penitipan_dialog.dart new file mode 100644 index 0000000..b966d01 --- /dev/null +++ b/lib/app/widgets/detail_penitipan_dialog.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; +import 'package:penyaluran_app/app/utils/date_formatter.dart'; + +/// Dialog untuk menampilkan detail penitipan bantuan +/// +/// Contoh penggunaan: +/// ```dart +/// // Di halaman lain +/// void showDetailPenitipan(BuildContext context, PenitipanBantuanModel item) { +/// // Dapatkan data yang diperlukan +/// final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan'; +/// final kategoriNama = item.kategoriBantuan?.nama ?? 'Kategori tidak ditemukan'; +/// final kategoriSatuan = item.kategoriBantuan?.satuan ?? ''; +/// +/// // Tampilkan dialog +/// DetailPenitipanDialog.show( +/// context: context, +/// item: item, +/// donaturNama: donaturNama, +/// kategoriNama: kategoriNama, +/// kategoriSatuan: kategoriSatuan, +/// getPetugasDesaNama: (String? id) => 'Nama Petugas', // Sesuaikan dengan cara mendapatkan nama petugas +/// showFullScreenImage: (String imageUrl) { +/// DetailPenitipanDialog.showFullScreenImage(context, imageUrl); +/// }, +/// ); +/// } +class DetailPenitipanDialog { + static void show({ + required BuildContext context, + required PenitipanBantuanModel item, + required String donaturNama, + required String kategoriNama, + required String kategoriSatuan, + required String Function(String?) getPetugasDesaNama, + required Function(String) showFullScreenImage, + }) { + // Cek apakah penitipan berbentuk uang + final isUang = item.isUang ?? false; + + Get.dialog( + AlertDialog( + title: const Text('Detail Penitipan'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailItem('Donatur', donaturNama), + _buildDetailItem('Status', item.status ?? 'Tidak diketahui'), + _buildDetailItem('Kategori Bantuan', kategoriNama), + _buildDetailItem( + 'Jumlah', + isUang + ? 'Rp ${DateFormatter.formatNumber(item.jumlah)}' + : '${DateFormatter.formatNumber(item.jumlah)} $kategoriSatuan'), + if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'), + _buildDetailItem( + 'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'), + _buildDetailItem( + 'Tanggal Penitipan', + DateFormatter.formatDateTime(item.tanggalPenitipan, + defaultValue: 'Tidak ada tanggal'), + ), + if (item.tanggalVerifikasi != null) + _buildDetailItem( + 'Tanggal Verifikasi', + DateFormatter.formatDateTime(item.tanggalVerifikasi), + ), + if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) + _buildDetailItem( + 'Diverifikasi Oleh', + getPetugasDesaNama(item.petugasDesaId) ?? 'Tidak diketahui', + ), + _buildDetailItem('Tanggal Dibuat', + DateFormatter.formatDateTime(item.createdAt)), + if (item.alasanPenolakan != null && + item.alasanPenolakan!.isNotEmpty) + _buildDetailItem('Alasan Penolakan', item.alasanPenolakan!), + + // Foto Bantuan + if (!isUang && + item.fotoBantuan != null && + item.fotoBantuan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text( + 'Foto Bantuan:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: item.fotoBantuan!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + showFullScreenImage(item.fotoBantuan![index]); + }, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + item.fotoBantuan![index], + height: 100, + width: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 100, + width: 100, + color: Colors.grey.shade300, + child: const Icon(Icons.error), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ), + + // Bukti Transfer (untuk bantuan uang) + if (isUang && + item.fotoBantuan != null && + item.fotoBantuan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text( + 'Bukti Transfer:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: item.fotoBantuan!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + showFullScreenImage(item.fotoBantuan![index]); + }, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + item.fotoBantuan![index], + height: 100, + width: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 100, + width: 100, + color: Colors.grey.shade300, + child: const Icon(Icons.error), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ), + + // Bukti Serah Terima + if (item.fotoBuktiSerahTerima != null && + item.fotoBuktiSerahTerima!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text( + 'Bukti Serah Terima:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () { + showFullScreenImage(item.fotoBuktiSerahTerima!); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + item.fotoBuktiSerahTerima!, + height: 200, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 200, + width: double.infinity, + color: Colors.grey.shade300, + child: const Icon(Icons.error), + ); + }, + ), + ), + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Tutup'), + ), + ], + ), + ); + } + + static Widget _buildDetailItem(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + Text( + value, + style: const TextStyle(fontSize: 14), + ), + const Divider(), + ], + ), + ); + } + + static void showFullScreenImage(BuildContext context, String imageUrl) { + Get.dialog( + Dialog( + insetPadding: EdgeInsets.zero, + child: Stack( + fit: StackFit.expand, + children: [ + InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4, + child: Image.network( + imageUrl, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey.shade300, + child: const Center( + child: Icon( + Icons.error, + size: 50, + color: Colors.red, + ), + ), + ); + }, + ), + ), + Positioned( + top: 20, + right: 20, + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); + } +}