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
This commit is contained in:
Khafidh Fuadi
2025-03-13 13:28:09 +07:00
parent 435435f9b6
commit d9cc7aaf92
9 changed files with 949 additions and 204 deletions

View File

@ -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<DonaturModel> daftarDonatur = <DonaturModel>[].obs;
final RxMap<String, List<PenitipanBantuanModel>> penitipanPerDonatur =
<String, List<PenitipanBantuanModel>>{}.obs;
final RxMap<String, StokBantuanModel> stokBantuanMap =
<String, StokBantuanModel>{}.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<void> 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
}
}

View File

@ -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<DonaturController> {
const DaftarDonaturView({super.key});
@ -199,8 +200,8 @@ class DaftarDonaturView extends GetView<DonaturController> {
),
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<DonaturController> {
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<DonaturController> {
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<DonaturController> {
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),

View File

@ -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<DonaturController> {
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<DonaturModel?>(
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);
},
);
}
}

View File

@ -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<PenitipanBantuanController> {
@ -694,198 +695,18 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
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);
},
);
}

View File

@ -298,7 +298,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
},
),
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

View File

@ -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<PenitipanBantuanController> {
const RiwayatPenitipanView({super.key});

View File

@ -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(),
),
];
}

View File

@ -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';
}

View File

@ -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,
),
),
),
),
],
),
),
);
}
}