Perbarui model dan tampilan untuk menambahkan properti fotoProfil di DonaturModel, PetugasDesaModel, dan WargaModel. Modifikasi controller dan tampilan untuk mendukung pengambilan dan penampilan foto profil pengguna. Tambahkan fungsionalitas baru untuk menampilkan foto profil di berbagai tampilan, termasuk detail penerima dan dashboard warga. Perbarui rute aplikasi untuk mencakup halaman profil pengguna.
This commit is contained in:
@ -161,173 +161,205 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
|
||||
|
||||
Widget _buildPenerimaCard(
|
||||
BuildContext context, Map<String, dynamic> penerima) {
|
||||
return Card(
|
||||
final statusActive = penerima['status'] == 'AKTIF';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// Navigasi ke halaman detail penerima
|
||||
Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Foto profil
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: penerima['foto'] != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Image.asset(
|
||||
penerima['foto'],
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.person,
|
||||
size: 30,
|
||||
color: AppTheme.primaryColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.person,
|
||||
size: 30,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// Navigasi ke halaman detail penerima
|
||||
Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white,
|
||||
AppTheme.primaryColor.withOpacity(0.03),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Informasi penerima
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penerima['nama'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Foto profil dengan animasi hero
|
||||
Hero(
|
||||
tag: 'penerima-${penerima['id']}',
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 35,
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
backgroundImage: penerima['foto_profil'] != null
|
||||
? NetworkImage(penerima['foto_profil'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
size: 35,
|
||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Informasi penerima
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penerima['nama_lengkap'] ?? 'Tanpa Nama',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (penerima['terverifikasi'] == true)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.verified,
|
||||
color: Colors.green,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
if (penerima['terverifikasi'] == true)
|
||||
child: Text(
|
||||
'NIK: ${penerima['nik'] ?? 'Belum Ada'}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: statusActive
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: statusActive
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: Colors.red.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.verified,
|
||||
size: 14,
|
||||
color: Colors.green,
|
||||
Icon(
|
||||
statusActive
|
||||
? Icons.check_circle
|
||||
: Icons.cancel,
|
||||
size: 12,
|
||||
color:
|
||||
statusActive ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Terverifikasi',
|
||||
Text(
|
||||
statusActive ? 'Aktif' : 'Tidak Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: statusActive
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'NIK: ${penerima['nik'] ?? ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
_buildStatusBadge(penerima['status']),
|
||||
// const SizedBox(height: 8),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text(
|
||||
// penerima['alamatLengkap'] ?? '',
|
||||
// style: TextStyle(
|
||||
// fontSize: 12,
|
||||
// color: Colors.grey[600],
|
||||
// ),
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// _buildStatusBadge(penerima['status']),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(String? status) {
|
||||
Color backgroundColor;
|
||||
Color textColor;
|
||||
|
||||
switch (status) {
|
||||
case 'Selesai':
|
||||
backgroundColor = Colors.green.withOpacity(0.1);
|
||||
textColor = Colors.green;
|
||||
break;
|
||||
case 'Terjadwal':
|
||||
backgroundColor = Colors.blue.withOpacity(0.1);
|
||||
textColor = Colors.blue;
|
||||
break;
|
||||
case 'Belum disalurkan':
|
||||
backgroundColor = Colors.orange.withOpacity(0.1);
|
||||
textColor = Colors.orange;
|
||||
break;
|
||||
default:
|
||||
backgroundColor = Colors.grey.withOpacity(0.1);
|
||||
textColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status ?? 'Tidak diketahui',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PenerimaSearchDelegate extends SearchDelegate {
|
||||
@ -369,7 +401,7 @@ class PenerimaSearchDelegate extends SearchDelegate {
|
||||
|
||||
Widget _buildSearchResults() {
|
||||
final filteredList = daftarPenerima.where((penerima) {
|
||||
final nama = penerima['nama']?.toString().toLowerCase() ?? '';
|
||||
final nama = penerima['nama_lengkap']?.toString().toLowerCase() ?? '';
|
||||
final nik = penerima['nik']?.toString().toLowerCase() ?? '';
|
||||
final alamat = penerima['alamatLengkap']?.toString().toLowerCase() ?? '';
|
||||
final searchLower = query.toLowerCase();
|
||||
@ -403,13 +435,18 @@ class PenerimaSearchDelegate extends SearchDelegate {
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
backgroundImage: penerima['foto_profil'] != null
|
||||
? NetworkImage(penerima['foto_profil'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: AppTheme.primaryColor,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text(
|
||||
penerima['nama'] ?? '',
|
||||
penerima['nama_lengkap'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
@ -79,12 +79,263 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
final totalNilaiDonasiUangFormatted =
|
||||
controller.formatRupiah(totalNilaiDonasiUang);
|
||||
|
||||
// Pilih warna berdasarkan jenis donatur
|
||||
Color jenisColor = donatur.jenis == 'Perusahaan'
|
||||
? Colors.blue
|
||||
: donatur.jenis == 'Organisasi'
|
||||
? Colors.green
|
||||
: Colors.orange;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header dengan informasi utama donatur
|
||||
// Header dengan informasi utama donatur - desain yang lebih menarik
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: jenisColor.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white,
|
||||
jenisColor.withOpacity(0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Avatar dan nama donatur dengan layout yang lebih baik
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: jenisColor.withOpacity(0.7), width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: jenisColor.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Hero(
|
||||
tag: 'donatur-${donatur.id}',
|
||||
child: CircleAvatar(
|
||||
radius: 45,
|
||||
backgroundColor: jenisColor.withOpacity(0.1),
|
||||
backgroundImage: donatur.fotoProfil != null &&
|
||||
donatur.fotoProfil!.isNotEmpty
|
||||
? NetworkImage(donatur.fotoProfil!)
|
||||
: null,
|
||||
child: (donatur.fotoProfil == null ||
|
||||
donatur.fotoProfil!.isEmpty)
|
||||
? Icon(
|
||||
jenisIcon,
|
||||
size: 45,
|
||||
color: jenisColor.withOpacity(0.8),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
donatur.nama ?? 'Tanpa Nama',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: jenisColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: jenisColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
jenisIcon,
|
||||
size: 16,
|
||||
color: jenisColor,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
donatur.jenis ?? 'Tidak Diketahui',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: jenisColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: donatur.status == 'AKTIF'
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: donatur.status == 'AKTIF'
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: Colors.red.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
donatur.status == 'AKTIF'
|
||||
? Icons.check_circle
|
||||
: Icons.cancel,
|
||||
size: 16,
|
||||
color: donatur.status == 'AKTIF'
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
donatur.status == 'AKTIF'
|
||||
? 'Aktif'
|
||||
: 'Tidak Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: donatur.status == 'AKTIF'
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Summary cards for donations
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSummaryCard(
|
||||
title: 'Total Donasi',
|
||||
value: jumlahDonasi.toString(),
|
||||
icon: Icons.volunteer_activism,
|
||||
color: jenisColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildSummaryCard(
|
||||
title: 'Donasi Uang',
|
||||
value: jumlahDonasiUang.toString(),
|
||||
icon: Icons.attach_money,
|
||||
color: jenisColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _buildSummaryCard(
|
||||
title: 'Donasi Barang',
|
||||
value: jumlahDonasiBarang.toString(),
|
||||
icon: Icons.inventory_2,
|
||||
color: jenisColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Total nilai donasi
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: jenisColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: jenisColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Total Nilai Donasi Uang',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
totalNilaiDonasiUangFormatted,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: jenisColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Informasi kontak
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
@ -94,104 +345,6 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
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',
|
||||
@ -215,85 +368,6 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
),
|
||||
),
|
||||
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,
|
||||
@ -315,12 +389,6 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman riwayat donasi lengkap
|
||||
},
|
||||
child: const Text('Lihat Semua'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@ -368,37 +436,51 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(
|
||||
String label, String value, IconData icon, Color color) {
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
child: Icon(
|
||||
Widget _buildSummaryCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
color: color.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
const DetailPenerimaView({super.key});
|
||||
@ -10,6 +11,11 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
Widget build(BuildContext context) {
|
||||
final String id = Get.arguments as String;
|
||||
|
||||
// Panggil metode untuk mengambil data penyaluran saat halaman dibuat
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchPenyaluranByWargaId(id);
|
||||
});
|
||||
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Scaffold(
|
||||
@ -51,6 +57,9 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
// Status penyaluran
|
||||
_buildStatusSection(penerima),
|
||||
|
||||
// Riwayat Penyaluran Bantuan
|
||||
_buildRiwayatPenyaluran(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
@ -63,65 +72,100 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
Widget _buildHeader(Map<String, dynamic> penerima) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 30, left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppTheme.primaryColor.withOpacity(0.8),
|
||||
AppTheme.primaryColor,
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Foto profil
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: Colors.white,
|
||||
child: penerima['foto'] != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset(
|
||||
penerima['foto'],
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.person,
|
||||
size: 50,
|
||||
color: AppTheme.primaryColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.person,
|
||||
size: 50,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
// Foto profil dengan efek bayangan dan border
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Hero(
|
||||
tag: 'penerima-${penerima['id']}',
|
||||
child: CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: penerima['foto_profil'] != null
|
||||
? NetworkImage(penerima['foto_profil'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
size: 60,
|
||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Nama penerima
|
||||
// Nama penerima dengan stroke effect
|
||||
Text(
|
||||
penerima['nama'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 5.0,
|
||||
color: Colors.black26,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// NIK
|
||||
Text(
|
||||
penerima['nik'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
// NIK dengan style yang lebih menarik
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'NIK: ${penerima['nik'] ?? 'Belum terdaftar'}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Badge terverifikasi
|
||||
// Badge terverifikasi dengan animasi
|
||||
if (penerima['terverifikasi'] == true)
|
||||
Container(
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
@ -129,13 +173,20 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.successColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified,
|
||||
size: 16,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
@ -143,12 +194,35 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
'Terverifikasi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Informasi status aktif
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: penerima['status'] == 'AKTIF'
|
||||
? Colors.green.withOpacity(0.2)
|
||||
: Colors.red.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
penerima['status'] == 'AKTIF' ? 'Aktif' : 'Tidak Aktif',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: penerima['status'] == 'AKTIF'
|
||||
? Colors.white
|
||||
: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -345,4 +419,252 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk menampilkan riwayat penyaluran bantuan
|
||||
Widget _buildRiwayatPenyaluran() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Riwayat Penyaluran Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(() {
|
||||
if (controller.isLoadingPenyaluran.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.daftarPenyaluran.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_rounded,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada riwayat penyaluran bantuan',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: controller.daftarPenyaluran.length,
|
||||
itemBuilder: (context, index) {
|
||||
final penyaluran = controller.daftarPenyaluran[index];
|
||||
return _buildPenyaluranItem(penyaluran);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk menampilkan item penyaluran bantuan
|
||||
Widget _buildPenyaluranItem(Map<String, dynamic> penyaluran) {
|
||||
final DateTime tanggalPenyaluran =
|
||||
DateTime.parse(penyaluran['tanggal_penyaluran']);
|
||||
final String formattedDate =
|
||||
DateFormat('dd MMMM yyyy', 'id_ID').format(tanggalPenyaluran);
|
||||
|
||||
final Color statusColor = penyaluran['status'] == 'TERLAKSANA'
|
||||
? AppTheme.completedColor
|
||||
: penyaluran['status'] == 'DIJADWALKAN'
|
||||
? AppTheme.processedColor
|
||||
: AppTheme.warningColor;
|
||||
|
||||
final IconData statusIcon = penyaluran['status'] == 'TERLAKSANA'
|
||||
? Icons.check_circle
|
||||
: penyaluran['status'] == 'DIJADWALKAN'
|
||||
? Icons.event
|
||||
: Icons.pending;
|
||||
|
||||
final Map<String, dynamic> stokBantuan =
|
||||
penyaluran['stok_bantuan'] as Map<String, dynamic>;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Baris atas dengan status dan tanggal
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Status penyaluran
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
statusIcon,
|
||||
color: statusColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
penyaluran['status'] == 'TERLAKSANA'
|
||||
? 'Terlaksana'
|
||||
: penyaluran['status'] == 'DIJADWALKAN'
|
||||
? 'Terjadwal'
|
||||
: 'Menunggu',
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Tanggal penyaluran
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// Informasi bantuan
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Ikon bantuan
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Detail bantuan
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
stokBantuan['nama'] ?? 'Bantuan',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${stokBantuan['jenis'] ?? 'Umum'} • ${stokBantuan['kuantitas'] ?? '1 Paket'}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
penyaluran['keterangan'] ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Tampilkan bukti penyaluran jika ada dan status TERLAKSANA
|
||||
if (penyaluran['status'] == 'TERLAKSANA' &&
|
||||
penyaluran['bukti_penyaluran'] != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 24),
|
||||
const Text(
|
||||
'Bukti Penyaluran',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
penyaluran['bukti_penyaluran'],
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 120,
|
||||
width: double.infinity,
|
||||
color: Colors.grey[200],
|
||||
child: const Center(
|
||||
child: Text('Gambar tidak tersedia'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Tampilkan tombol riwayat hanya jika tab Penitipan aktif
|
||||
if (activeTab == 2) {
|
||||
return Row(
|
||||
@ -167,149 +168,336 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
|
||||
Widget _buildDrawer(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
DrawerHeader(
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 24,
|
||||
left: 16,
|
||||
right: 16),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Hero(
|
||||
tag: 'profile-photo',
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white70,
|
||||
backgroundImage: controller.profilePhotoUrl != null
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Halo,',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
controller.nama,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.user?.desa?.nama != null
|
||||
? '${controller.formattedRole} - ${controller.user!.desa!.nama}'
|
||||
: controller.formattedRole,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withAlpha(200),
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
controller.formattedRole,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
controller.desa,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
selected: controller.activeTabIndex.value == 0,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(0);
|
||||
},
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_buildMenuCategory('Menu Utama'),
|
||||
Obx(() => _buildMenuItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
title: 'Dashboard',
|
||||
isSelected: controller.activeTabIndex.value == 0,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(0);
|
||||
},
|
||||
)),
|
||||
Obx(() => _buildMenuItem(
|
||||
icon: Icons.handshake_outlined,
|
||||
activeIcon: Icons.handshake,
|
||||
title: 'Penyaluran',
|
||||
isSelected: controller.activeTabIndex.value == 1,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(1);
|
||||
},
|
||||
)),
|
||||
Obx(() => _buildMenuItem(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
activeIcon: Icons.inventory_2,
|
||||
title: 'Penitipan',
|
||||
isSelected: controller.activeTabIndex.value == 2,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(2);
|
||||
},
|
||||
)),
|
||||
Obx(() => _buildMenuItem(
|
||||
icon: Icons.warning_amber_outlined,
|
||||
activeIcon: Icons.warning_amber,
|
||||
title: 'Pengaduan',
|
||||
isSelected: controller.activeTabIndex.value == 3,
|
||||
badge: controller.jumlahDiproses.value > 0
|
||||
? controller.jumlahDiproses.value.toString()
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(3);
|
||||
},
|
||||
)),
|
||||
Obx(() => _buildMenuItem(
|
||||
icon: Icons.inventory_outlined,
|
||||
activeIcon: Icons.inventory,
|
||||
title: 'Stok Bantuan',
|
||||
isSelected: controller.activeTabIndex.value == 4,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(4);
|
||||
},
|
||||
)),
|
||||
_buildMenuCategory('Kelola Data'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.person_add_outlined,
|
||||
activeIcon: Icons.person_add,
|
||||
title: 'Kelola Penerima',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-penerima');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.people_outlined,
|
||||
activeIcon: Icons.people,
|
||||
title: 'Kelola Donatur',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-donatur');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.description_outlined,
|
||||
activeIcon: Icons.description,
|
||||
title: 'Laporan Penyaluran',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/laporan-penyaluran');
|
||||
},
|
||||
),
|
||||
_buildMenuCategory('Pengaturan'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.person_outline,
|
||||
activeIcon: Icons.person,
|
||||
title: 'Profil',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.logout,
|
||||
title: 'Keluar',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.logout();
|
||||
},
|
||||
isLogout: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.handshake_outlined),
|
||||
title: const Text('Penyaluran'),
|
||||
selected: controller.activeTabIndex.value == 1,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(1);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Penitipan'),
|
||||
selected: controller.activeTabIndex.value == 2,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(2);
|
||||
},
|
||||
),
|
||||
Obx(() => ListTile(
|
||||
leading: controller.jumlahDiproses.value > 0
|
||||
? Badge(
|
||||
label: Text(controller.jumlahDiproses.value.toString()),
|
||||
backgroundColor: Colors.red,
|
||||
child: const Icon(Icons.warning_amber_outlined),
|
||||
)
|
||||
: const Icon(Icons.warning_amber_outlined),
|
||||
title: const Text('Pengaduan'),
|
||||
selected: controller.activeTabIndex.value == 3,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(3);
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_outlined),
|
||||
title: const Text('Stok Bantuan'),
|
||||
selected: controller.activeTabIndex.value == 4,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(4);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: const Text('Kelola Penerima'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-penerima');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people_outlined),
|
||||
title: const Text('Kelola Donatur'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-donatur');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: const Text('Laporan Penyaluran'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/laporan-penyaluran');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.logout();
|
||||
},
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuCategory(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
IconData? activeIcon,
|
||||
required String title,
|
||||
bool isSelected = false,
|
||||
String? badge,
|
||||
required Function() onTap,
|
||||
bool isLogout = false,
|
||||
}) {
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppTheme.primaryColor.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? (activeIcon ?? icon) : icon,
|
||||
color: isSelected
|
||||
? AppTheme.primaryColor
|
||||
: isLogout
|
||||
? Colors.red
|
||||
: Colors.grey[700],
|
||||
size: 24,
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 16,
|
||||
minHeight: 16,
|
||||
),
|
||||
child: Text(
|
||||
badge,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected
|
||||
? AppTheme.primaryColor
|
||||
: isLogout
|
||||
? Colors.red
|
||||
: Colors.grey[800],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
selectedTileColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
selected: isSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavigationBar() {
|
||||
return Obx(() {
|
||||
return BottomNavigationBar(
|
||||
@ -456,29 +644,31 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
children: [
|
||||
const Icon(Icons.report_problem_outlined),
|
||||
// Selalu tampilkan badge untuk debugging
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahDiproses.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
|
||||
if (controller.jumlahDiproses.value > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahDiproses.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
activeIcon: Stack(
|
||||
|
Reference in New Issue
Block a user