Perbarui model Pengaduan dengan menambahkan getter isUang untuk memeriksa jenis bantuan. Modifikasi tampilan dan controller di modul donatur dan petugas desa untuk meningkatkan pengalaman pengguna, termasuk penggantian ikon dan penyesuaian format tampilan jumlah bantuan. Hapus kode yang tidak diperlukan untuk menjaga kebersihan kode.

This commit is contained in:
Khafidh Fuadi
2025-04-10 14:25:41 +07:00
parent 3f78514175
commit ca6c28f3d6
40 changed files with 3103 additions and 2270 deletions

View File

@ -263,7 +263,7 @@ class DaftarDonaturView extends GetView<DonaturController> {
child: Row(
children: [
const Icon(
Icons.attach_money,
Icons.payment_rounded,
size: 14,
color: Colors.green,
),

File diff suppressed because it is too large Load Diff

View File

@ -277,7 +277,7 @@ class DetailDonaturView extends GetView<DonaturController> {
child: _buildSummaryCard(
title: 'Donasi Uang',
value: jumlahDonasiUang.toString(),
icon: Icons.attach_money,
icon: Icons.payment_rounded,
color: jenisColor,
),
),

View File

@ -5,14 +5,10 @@ import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
@ -1391,61 +1387,9 @@ class DetailPengaduanView extends GetView<PengaduanController> {
? List<String>.from(tindakan.buktiTindakan!)
: [];
// Fungsi untuk memilih bukti tindakan
Future<void> pickBuktiTindakan(
BuildContext dialogContext, bool fromCamera) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
imageQuality: 80,
maxWidth: 1200,
maxHeight: 1200,
preferredCameraDevice:
fromCamera ? CameraDevice.rear : CameraDevice.front,
);
if (pickedFile != null) {
// Tampilkan loading dialog
showDialog(
context: dialogContext,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
try {
// Tambahkan gambar ke daftar
buktiTindakanPaths.add(pickedFile.path);
// Tutup loading dialog
Navigator.of(dialogContext, rootNavigator: true).pop();
// Tutup dialog pilih sumber foto
Navigator.of(dialogContext).pop();
} catch (e) {
// Tutup loading dialog jika terjadi error
Navigator.of(dialogContext, rootNavigator: true).pop();
rethrow;
}
}
} catch (e) {
print('Error picking image: $e');
Get.snackbar(
'Error',
'Gagal mengambil gambar: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Fungsi untuk menampilkan dialog pilih sumber foto
void showPilihSumberFoto(BuildContext dialogContext) {
void showPilihSumberFoto(
BuildContext dialogContext, Function(BuildContext, bool) pickFunction) {
showDialog(
context: dialogContext,
builder: (innerContext) => AlertDialog(
@ -1456,12 +1400,12 @@ class DetailPengaduanView extends GetView<PengaduanController> {
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Kamera'),
onTap: () => pickBuktiTindakan(innerContext, true),
onTap: () => pickFunction(innerContext, true),
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Galeri'),
onTap: () => pickBuktiTindakan(innerContext, false),
onTap: () => pickFunction(innerContext, false),
),
],
),
@ -1473,6 +1417,61 @@ class DetailPengaduanView extends GetView<PengaduanController> {
context: context,
builder: (dialogContext) =>
StatefulBuilder(builder: (stateContext, setState) {
// Fungsi untuk memilih bukti tindakan dipindahkan ke dalam StatefulBuilder
Future<void> pickBuktiTindakan(
BuildContext innerContext, bool fromCamera) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
imageQuality: 80,
maxWidth: 1200,
maxHeight: 1200,
preferredCameraDevice:
fromCamera ? CameraDevice.rear : CameraDevice.front,
);
if (pickedFile != null) {
// Tampilkan loading dialog
showDialog(
context: innerContext,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
try {
// Tambahkan gambar ke daftar dan update state
setState(() {
buktiTindakanPaths.add(pickedFile.path);
});
// Tutup loading dialog
Navigator.of(innerContext, rootNavigator: true).pop();
// Tutup dialog pilih sumber foto
Navigator.of(innerContext).pop();
} catch (e) {
// Tutup loading dialog jika terjadi error
Navigator.of(innerContext, rootNavigator: true).pop();
rethrow;
}
}
} catch (e) {
print('Error picking image: $e');
Get.snackbar(
'Error',
'Gagal mengambil gambar: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
return AlertDialog(
title: Row(
children: [
@ -1581,7 +1580,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
children: [
if (buktiTindakanPaths.isEmpty)
InkWell(
onTap: () => showPilihSumberFoto(stateContext),
onTap: () => showPilihSumberFoto(
stateContext, pickBuktiTindakan),
child: Container(
height: 150,
width: double.infinity,
@ -1619,15 +1619,16 @@ class DetailPengaduanView extends GetView<PengaduanController> {
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: buktiTindakanPaths
.length, //tombol tambah jika tidak selesai
itemCount: buktiTindakanPaths.length +
1, // Tambah 1 untuk tombol tambah
itemBuilder: (context, index) {
if (index ==
buktiTindakanPaths.length) {
// Tombol tambah foto
return InkWell(
onTap: () => showPilihSumberFoto(
stateContext),
stateContext,
pickBuktiTindakan),
child: Container(
width: 100,
margin: const EdgeInsets.only(

View File

@ -675,7 +675,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
borderRadius: BorderRadius.circular(6),
),
child: Icon(
isUang ? Icons.attach_money : Icons.scale_outlined,
isUang ? Icons.payment_rounded : Icons.scale_outlined,
color: Colors.green,
size: 20,
),

View File

@ -521,7 +521,15 @@ class PengaduanView extends GetView<PengaduanController> {
),
),
Text(
'${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}',
item.stokBantuan?['is_uang'] == true
? FormatHelper.formatRupiah(
item.jumlahBantuan is num
? item.jumlahBantuan
: double.tryParse(item
.jumlahBantuan
.toString()) ??
0)
: '${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,

View File

@ -10,6 +10,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dar
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
import 'package:penyaluran_app/app/widgets/app_drawer.dart';
class PetugasDesaView extends GetView<PetugasDesaController> {
const PetugasDesaView({super.key});
@ -190,360 +191,119 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
}
Widget _buildDrawer(BuildContext context) {
return Drawer(
child: Column(
children: [
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,
children: [
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 &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty)
? Text(
controller.nama.isNotEmpty
? controller.nama
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 30,
),
)
: null,
),
),
),
SizedBox(height: 16),
Text(
'Halo,',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
Text(
controller.nama,
style: TextStyle(
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,
),
),
],
),
),
],
),
],
),
return Obx(() {
Map<String, List<DrawerMenuItem>> menuCategories = {
'Menu Utama': [
DrawerMenuItem(
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () {
controller.activeTabIndex.value = 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,
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.location_on_outlined,
activeIcon: Icons.location_on,
title: 'Lokasi Penyaluran',
onTap: () {
Navigator.pop(context);
Get.toNamed('/lokasi-penyaluran');
},
),
_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');
},
),
const Divider(),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
onTap: () {
Navigator.pop(context);
controller.logout();
},
isLogout: true,
),
],
),
DrawerMenuItem(
icon: Icons.volunteer_activism_outlined,
activeIcon: Icons.volunteer_activism,
title: 'Penyaluran',
isSelected: controller.activeTabIndex.value == 1,
badgeCount: controller.jumlahMenunggu.value > 0
? controller.jumlahMenunggu.value
: null,
badgeColor: Colors.green,
onTap: () {
controller.activeTabIndex.value = 1;
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
DrawerMenuItem(
icon: Icons.inbox_outlined,
activeIcon: Icons.inbox,
title: 'Penitipan',
isSelected: controller.activeTabIndex.value == 2,
onTap: () {
controller.activeTabIndex.value = 2;
},
),
DrawerMenuItem(
icon: Icons.report_problem_outlined,
activeIcon: Icons.report_problem,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 3,
badgeCount: controller.jumlahDiproses.value > 0
? controller.jumlahDiproses.value
: null,
badgeColor: Colors.orange,
onTap: () {
controller.activeTabIndex.value = 3;
},
),
DrawerMenuItem(
icon: Icons.inventory_outlined,
activeIcon: Icons.inventory,
title: 'Stok Bantuan',
isSelected: controller.activeTabIndex.value == 4,
onTap: () {
controller.activeTabIndex.value = 4;
},
),
],
),
);
}
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,
),
'Pengaturan': [
DrawerMenuItem(
icon: Icons.notifications_outlined,
activeIcon: Icons.notifications,
title: 'Notifikasi',
badgeCount: controller.jumlahNotifikasiBelumDibaca.value > 0
? controller.jumlahNotifikasiBelumDibaca.value
: null,
badgeColor: Colors.red,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotifikasiView(),
),
),
],
),
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,
),
);
DrawerMenuItem(
icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () {
Get.toNamed('/profile');
},
),
DrawerMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Get.toNamed('/about');
},
),
DrawerMenuItem(
icon: Icons.logout,
title: 'Keluar',
isLogout: true,
onTap: () {
controller.logout();
},
),
],
};
return AppDrawer(
nama: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.desa,
avatar: controller.profilePhotoUrl,
menuItems: const [], // Tidak digunakan karena menggunakan menuCategories
menuCategories: menuCategories,
onLogout: controller.logout,
footerText: '© ${DateTime.now().year} DisalurKita',
);
});
}
Widget _buildBottomNavigationBar() {

View File

@ -347,10 +347,16 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
),
),
...controller.daftarStokBantuan.map((stok) {
final bool isUang = stok.isUang ?? false;
final String formattedJumlah = isUang
? FormatHelper.formatRupiah(
stok.totalStok ?? 0)
: '${stok.totalStok} ${stok.satuan}';
return DropdownMenuItem(
value: stok.id,
child: Text(
stok.nama ?? '-',
'${stok.nama ?? '-'} ($formattedJumlah)',
overflow: TextOverflow.ellipsis,
),
);
@ -380,6 +386,9 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
: 'Tidak diketahui';
final stokBantuanSatuan =
riwayat.stokBantuan != null ? riwayat.stokBantuan!['satuan'] ?? '' : '';
final bool isUang = riwayat.stokBantuan != null
? riwayat.stokBantuan!['is_uang'] ?? false
: false;
final sumberLabels = {
'penitipan': 'Penitipan',
'penerimaan': 'Penerimaan',
@ -464,7 +473,9 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
),
const SizedBox(width: 4),
Text(
'${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan',
isUang
? FormatHelper.formatRupiah(riwayat.jumlah ?? 0)
: '${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isPenambahan ? Colors.green : Colors.red,
@ -761,10 +772,13 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
value: controller.selectedStokBantuan.value,
items: controller.daftarStokBantuan
.map((StokBantuanModel stok) {
final bool isUang = stok.isUang ?? false;
final String formattedStok = isUang
? FormatHelper.formatRupiah(stok.totalStok ?? 0)
: '${stok.totalStok} ${stok.satuan}';
return DropdownMenuItem<StokBantuanModel>(
value: stok,
child: Text(
'${stok.nama} (${stok.totalStok} ${stok.satuan})'),
child: Text('${stok.nama} ($formattedStok)'),
);
}).toList(),
onChanged: (StokBantuanModel? value) {

View File

@ -24,7 +24,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
},
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Stok', style: TextStyle(color: Colors.white)),
label: const Text('Tambah Jenis Stok',
style: TextStyle(color: Colors.white)),
elevation: 2,
),
);
@ -636,7 +637,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tambah Stok Bantuan',
'Tambah Jenis Stok Bantuan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),

View File

@ -363,7 +363,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
child: Row(
children: [
isUang.value
? const Icon(Icons.attach_money)
? const Icon(Icons.payment_rounded)
: const Icon(Icons.inventory_2),
const SizedBox(width: 8),
Expanded(