Tambahkan dukungan bantuan uang dan perhitungan total dana bantuan

- Perbarui model PenitipanBantuanModel dan StokBantuanModel dengan properti isUang
- Hapus field tanggal kadaluarsa dan tanggal masuk yang tidak digunakan
- Tambahkan metode _hitungTotalDanaBantuan() di StokBantuanController
- Perbarui tampilan untuk mendukung bantuan berbentuk uang
- Modifikasi form tambah/edit stok bantuan untuk menandai bantuan uang
- Tambahkan metode getPenitipanBantuanTerverifikasi() di SupabaseService
- Perbarui perhitungan total stok berdasarkan penitipan bantuan terverifikasi
This commit is contained in:
Khafidh Fuadi
2025-03-12 08:42:00 +07:00
parent 8a3b23d4ea
commit 8d5fb275e8
6 changed files with 469 additions and 345 deletions

View File

@ -15,12 +15,12 @@ class PenitipanBantuanModel {
final DateTime? tanggalVerifikasi;
final DateTime? createdAt;
final DateTime? updatedAt;
final DateTime? tanggalKadaluarsa;
final String? petugasDesaId;
final String? fotoBuktiSerahTerima;
final String? sumberBantuanId;
final DonaturModel? donatur;
final KategoriBantuanModel? kategoriBantuan;
final bool? isUang;
PenitipanBantuanModel({
this.id,
@ -35,12 +35,12 @@ class PenitipanBantuanModel {
this.tanggalVerifikasi,
this.createdAt,
this.updatedAt,
this.tanggalKadaluarsa,
this.petugasDesaId,
this.fotoBuktiSerahTerima,
this.sumberBantuanId,
this.donatur,
this.kategoriBantuan,
this.isUang,
});
factory PenitipanBantuanModel.fromRawJson(String str) =>
@ -72,9 +72,6 @@ class PenitipanBantuanModel {
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
tanggalKadaluarsa: json["tanggal_kadaluarsa"] != null
? DateTime.parse(json["tanggal_kadaluarsa"])
: null,
petugasDesaId: json["petugas_desa_id"],
fotoBuktiSerahTerima: json["foto_bukti_serah_terima"],
sumberBantuanId: json["sumber_bantuan_id"],
@ -84,6 +81,7 @@ class PenitipanBantuanModel {
kategoriBantuan: json["kategori_bantuan"] != null
? KategoriBantuanModel.fromJson(json["kategori_bantuan"])
: null,
isUang: json["is_uang"] ?? false,
);
Map<String, dynamic> toJson() => {
@ -101,9 +99,9 @@ class PenitipanBantuanModel {
"tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
"tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(),
"petugas_desa_id": petugasDesaId,
"foto_bukti_serah_terima": fotoBuktiSerahTerima,
"sumber_bantuan_id": sumberBantuanId,
"is_uang": isUang ?? false,
};
}

View File

@ -8,10 +8,9 @@ class StokBantuanModel {
final double? totalStok;
final String? satuan;
final String? deskripsi;
final DateTime? tanggalMasuk;
final DateTime? tanggalKadaluarsa;
final DateTime? createdAt;
final DateTime? updatedAt;
final bool? isUang;
StokBantuanModel({
this.id,
@ -21,10 +20,9 @@ class StokBantuanModel {
this.totalStok,
this.satuan,
this.deskripsi,
this.tanggalMasuk,
this.tanggalKadaluarsa,
this.createdAt,
this.updatedAt,
this.isUang,
});
factory StokBantuanModel.fromRawJson(String str) =>
@ -38,35 +36,27 @@ class StokBantuanModel {
nama: json["nama"],
kategoriBantuanId: json["kategori_bantuan_id"],
kategoriBantuan: json["kategori_bantuan"],
totalStok:
json["total_stok"] != null ? json["total_stok"].toDouble() : 0.0,
totalStok: 0.0,
satuan: json["satuan"],
deskripsi: json["deskripsi"],
tanggalMasuk: json["tanggal_masuk"] != null
? DateTime.parse(json["tanggal_masuk"])
: null,
tanggalKadaluarsa: json["tanggal_kadaluarsa"] != null
? DateTime.parse(json["tanggal_kadaluarsa"])
: null,
createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"])
: null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
isUang: json["is_uang"] ?? false,
);
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {
"nama": nama,
"kategori_bantuan_id": kategoriBantuanId,
"total_stok": totalStok,
"satuan": satuan,
"deskripsi": deskripsi,
"tanggal_masuk": tanggalMasuk?.toIso8601String(),
"tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
"is_uang": isUang ?? false,
};
// Tambahkan id hanya jika tidak null

View File

@ -13,9 +13,10 @@ class StokBantuanController extends GetxController {
// Data untuk stok bantuan
final RxList<StokBantuanModel> daftarStokBantuan = <StokBantuanModel>[].obs;
final RxDouble totalStok = 0.0.obs;
final RxDouble stokMasuk = 0.0.obs;
final RxDouble stokKeluar = 0.0.obs;
// Data untuk penitipan bantuan terverifikasi
final RxList<Map<String, dynamic>> daftarPenitipanTerverifikasi =
<Map<String, dynamic>>[].obs;
// Data untuk kategori bantuan
final RxList<Map<String, dynamic>> daftarKategoriBantuan =
@ -25,6 +26,9 @@ class StokBantuanController extends GetxController {
final TextEditingController searchController = TextEditingController();
final RxString searchQuery = ''.obs;
// Tambahkan properti untuk total dana bantuan
RxDouble totalDanaBantuan = 0.0.obs;
UserModel? get user => _authController.user;
@override
@ -32,6 +36,7 @@ class StokBantuanController extends GetxController {
super.onInit();
loadStokBantuanData();
loadKategoriBantuanData();
loadPenitipanTerverifikasi();
// Listener untuk pencarian
searchController.addListener(() {
@ -54,18 +59,8 @@ class StokBantuanController extends GetxController {
.map((data) => StokBantuanModel.fromJson(data))
.toList();
// Hitung total stok
totalStok.value = 0;
for (var item in daftarStokBantuan) {
totalStok.value += item.totalStok ?? 0;
}
// Ambil data stok masuk dan keluar
final stokData = await _supabaseService.getStokStatistics();
if (stokData != null) {
stokMasuk.value = stokData['masuk'] ?? 0;
stokKeluar.value = stokData['keluar'] ?? 0;
}
// Hitung total dana bantuan
_hitungTotalDanaBantuan();
}
} catch (e) {
print('Error loading stok bantuan data: $e');
@ -74,6 +69,71 @@ class StokBantuanController extends GetxController {
}
}
Future<void> loadPenitipanTerverifikasi() async {
try {
final penitipanData =
await _supabaseService.getPenitipanBantuanTerverifikasi();
if (penitipanData != null) {
daftarPenitipanTerverifikasi.value = penitipanData;
// Update total stok berdasarkan penitipan terverifikasi
_hitungTotalStokDariPenitipan();
}
} catch (e) {
print('Error loading penitipan terverifikasi: $e');
}
}
// Metode untuk menghitung total stok dari penitipan terverifikasi
void _hitungTotalStokDariPenitipan() {
// Buat map untuk menyimpan total stok per stok_bantuan_id
Map<String, double> totalStokMap = {};
// Hitung total stok dari penitipan terverifikasi
for (var penitipan in daftarPenitipanTerverifikasi) {
String? stokBantuanId = penitipan['stok_bantuan_id'];
double jumlah = penitipan['jumlah'] != null
? (penitipan['jumlah'] is int
? penitipan['jumlah'].toDouble()
: penitipan['jumlah'])
: 0.0;
if (stokBantuanId != null) {
if (totalStokMap.containsKey(stokBantuanId)) {
totalStokMap[stokBantuanId] =
(totalStokMap[stokBantuanId] ?? 0) + jumlah;
} else {
totalStokMap[stokBantuanId] = jumlah;
}
}
}
// Update total stok di daftarStokBantuan
for (var i = 0; i < daftarStokBantuan.length; i++) {
var stok = daftarStokBantuan[i];
if (stok.id != null) {
// Buat stok baru dengan total stok yang diperbarui
double newTotalStok = totalStokMap[stok.id] ?? 0.0;
daftarStokBantuan[i] = StokBantuanModel(
id: stok.id,
nama: stok.nama,
kategoriBantuanId: stok.kategoriBantuanId,
kategoriBantuan: stok.kategoriBantuan,
totalStok:
newTotalStok, // Gunakan nilai dari penitipan atau 0 jika tidak ada
satuan: stok.satuan,
deskripsi: stok.deskripsi,
createdAt: stok.createdAt,
updatedAt: stok.updatedAt,
isUang: stok.isUang,
);
}
}
// Hitung ulang total dana bantuan
_hitungTotalDanaBantuan();
}
Future<void> loadKategoriBantuanData() async {
try {
final kategoriBantuanData = await _supabaseService.getKategoriBantuan();
@ -86,13 +146,21 @@ class StokBantuanController extends GetxController {
}
Future<void> addStok(StokBantuanModel stok) async {
isLoading.value = true;
try {
await _supabaseService.addStok(stok.toJson());
// Buat data stok baru tanpa field total_stok
final stokData = stok.toJson();
// Hapus field total_stok dari data yang akan dikirim ke database
if (stokData.containsKey('total_stok')) {
stokData.remove('total_stok');
}
await _supabaseService.addStok(stokData);
await loadStokBantuanData();
await loadPenitipanTerverifikasi();
Get.snackbar(
'Sukses',
'Stok berhasil ditambahkan',
'Stok bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
@ -101,24 +169,30 @@ class StokBantuanController extends GetxController {
print('Error adding stok: $e');
Get.snackbar(
'Error',
'Gagal menambahkan stok: ${e.toString()}',
'Gagal menambahkan stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> updateStok(StokBantuanModel stok) async {
isLoading.value = true;
try {
await _supabaseService.updateStok(stok.id ?? '', stok.toJson());
// Buat data stok untuk update
final stokData = stok.toJson();
// Hapus field total_stok dari data yang akan dikirim ke database
if (stokData.containsKey('total_stok')) {
stokData.remove('total_stok');
}
await _supabaseService.updateStok(stok.id ?? '', stokData);
await loadStokBantuanData();
await loadPenitipanTerverifikasi();
Get.snackbar(
'Sukses',
'Stok berhasil diperbarui',
'Stok bantuan berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
@ -127,24 +201,22 @@ class StokBantuanController extends GetxController {
print('Error updating stok: $e');
Get.snackbar(
'Error',
'Gagal memperbarui stok: ${e.toString()}',
'Gagal memperbarui stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> deleteStok(String stokId) async {
isLoading.value = true;
Future<void> deleteStok(String id) async {
try {
await _supabaseService.deleteStok(stokId);
await loadStokBantuanData();
await _supabaseService.deleteStok(id);
await loadStokBantuanData(); // Ini akan memanggil _hitungTotalDanaBantuan()
await loadPenitipanTerverifikasi(); // Perbarui data penitipan terverifikasi
Get.snackbar(
'Sukses',
'Stok berhasil dihapus',
'Stok bantuan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
@ -153,24 +225,19 @@ class StokBantuanController extends GetxController {
print('Error deleting stok: $e');
Get.snackbar(
'Error',
'Gagal menghapus stok: ${e.toString()}',
'Gagal menghapus stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadStokBantuanData();
await loadKategoriBantuanData();
} finally {
isLoading.value = false;
}
await loadStokBantuanData();
await loadPenitipanTerverifikasi();
isLoading.value = false;
}
List<StokBantuanModel> getFilteredStokBantuan() {
@ -202,12 +269,22 @@ class StokBantuanController extends GetxController {
.length;
}
// Metode untuk mendapatkan jumlah stok yang segera kadaluarsa (dalam 30 hari)
int getStokSegeraKadaluarsa() {
return daftarStokBantuan
.where((stok) =>
stok.tanggalKadaluarsa != null &&
stok.tanggalKadaluarsa!.difference(DateTime.now()).inDays <= 30)
.length;
// Metode untuk menghitung total dana bantuan
void _hitungTotalDanaBantuan() {
double total = 0.0;
for (var stok in daftarStokBantuan) {
if (stok.isUang == true) {
total += stok.totalStok ?? 0.0;
}
}
totalDanaBantuan.value = total;
}
Future<void> _hitungTotalStok() async {
// Implementasi metode _hitungTotalStok
}
Future<void> _filterStokBantuan() async {
// Implementasi metode _filterStokBantuan
}
}

View File

@ -294,6 +294,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
print(
'PenitipanItem - kategoriNama: $kategoriNama, kategoriSatuan: $kategoriSatuan');
// Cek apakah penitipan berbentuk uang
final isUang = item.isUang ?? false;
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
@ -360,7 +363,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Expanded(
child: _buildItemDetail(
context,
icon: Icons.category,
icon: isUang ? Icons.monetization_on : Icons.category,
label: 'Kategori Bantuan',
value: kategoriNama,
),
@ -368,10 +371,12 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
icon:
isUang ? Icons.account_balance_wallet : Icons.inventory,
label: 'Jumlah',
value:
'${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}',
value: isUang
? 'Rp ${DateFormatter.formatNumber(item.jumlah)}'
: '${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}',
),
),
],
@ -381,7 +386,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.calendar_today,
label: 'Tanggal Penitipan',
value: DateFormatter.formatDate(item.tanggalPenitipan,
value: DateFormatter.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'),
),
@ -673,6 +678,9 @@ 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'),
@ -684,36 +692,39 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
_buildDetailItem('Donatur', donaturNama),
_buildDetailItem('Status', item.status ?? 'Tidak diketahui'),
_buildDetailItem('Kategori Bantuan', kategoriNama),
_buildDetailItem('Jumlah',
'${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}'),
_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.formatDate(item.tanggalPenitipan,
DateFormatter.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'),
),
if (item.tanggalVerifikasi != null)
_buildDetailItem(
'Tanggal Verifikasi',
DateFormatter.formatDate(item.tanggalVerifikasi),
DateFormatter.formatDateTime(item.tanggalVerifikasi),
),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem(
'Diverifikasi Oleh',
controller.getPetugasDesaNama(item.petugasDesaId),
),
if (item.tanggalKadaluarsa != null)
_buildDetailItem(
'Tanggal Kadaluarsa',
DateFormatter.formatDate(item.tanggalKadaluarsa),
),
_buildDetailItem('Tanggal Masuk',
DateFormatter.formatDateTime(item.tanggalPenitipan)),
if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
// Foto Bantuan
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty)
if (!isUang &&
item.fotoBantuan != null &&
item.fotoBantuan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -761,6 +772,57 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
],
),
// 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)

View File

@ -72,37 +72,15 @@ class StokBantuanView extends GetView<StokBantuanController> {
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Stok Tersedia',
value: DateFormatter.formatNumber(controller.totalStok.value),
const SizedBox(height: 4),
Text(
'Data stok diambil dari penitipan bantuan terverifikasi',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.input,
title: 'Total Masuk',
value: DateFormatter.formatNumber(controller.stokMasuk.value),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.output,
title: 'Total Keluar',
value:
DateFormatter.formatNumber(controller.stokKeluar.value),
),
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 16),
Row(
children: [
Expanded(
@ -119,24 +97,74 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.access_time,
title: 'Segera Kadaluarsa',
value: '${controller.getStokSegeraKadaluarsa()}',
valueColor: controller.getStokSegeraKadaluarsa() > 0
? Colors.amber
: Colors.white,
icon: Icons.handshake_outlined,
title: 'Penitipan',
value: '${controller.daftarPenitipanTerverifikasi.length}',
valueColor: Colors.white,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.category_outlined,
title: 'Kategori Bantuan',
value: '${controller.daftarKategoriBantuan.length}',
icon: Icons.inventory_2,
title: 'Jenis Bantuan',
value: '${controller.daftarStokBantuan.length}',
),
),
],
),
// Tampilkan total dana bantuan jika ada
if (controller.totalDanaBantuan.value > 0) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: const Icon(
Icons.monetization_on,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Dana Bantuan',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white,
),
),
Text(
'Rp ${DateFormatter.formatNumber(controller.totalDanaBantuan.value)}',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
),
),
],
],
),
);
@ -311,15 +339,27 @@ class StokBantuanView extends GetView<StokBantuanController> {
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.kategoriBantuan != null
? (item.kategoriBantuan!['nama'] ??
'Tidak Ada Kategori')
: 'Tidak Ada Kategori',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.isUang == true)
const Icon(
Icons.monetization_on,
size: 16,
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
if (item.isUang == true) const SizedBox(width: 4),
Text(
item.kategoriBantuan != null
? (item.kategoriBantuan!['nama'] ??
'Tidak Ada Kategori')
: 'Tidak Ada Kategori',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
@ -340,31 +380,13 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Total Stok',
value:
'${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Masuk',
value: DateFormatter.formatDateTime(item.tanggalMasuk),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.timelapse,
label: 'Kadaluarsa',
value: DateFormatter.formatDate(item.tanggalKadaluarsa),
icon: item.isUang == true
? Icons.monetization_on
: Icons.inventory,
label: item.isUang == true ? 'Total Dana' : 'Total Stok',
value: item.isUang == true
? 'Rp ${DateFormatter.formatNumber(item.totalStok)}'
: '${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
),
),
Expanded(
@ -452,14 +474,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
void _showAddStokDialog(BuildContext context) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController();
final stokController = TextEditingController();
final satuanController = TextEditingController();
final deskripsiController = TextEditingController();
String? selectedJenisBantuanId;
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime tanggalMasuk = DateTime.now();
DateTime? tanggalKadaluarsa;
bool isUang = false;
showDialog(
context: context,
@ -510,46 +528,39 @@ class StokBantuanView extends GetView<StokBantuanController> {
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: stokController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
// Tambahkan checkbox untuk menandai sebagai uang
CheckboxListTile(
title: const Text('Bantuan Berbentuk Uang (Rupiah)'),
value: isUang,
onChanged: (value) {
setState(() {
isUang = value ?? false;
if (isUang) {
satuanController.text = 'Rp';
} else {
satuanController.text = '';
}
});
},
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 16),
// Hapus input jumlah/stok dan hanya tampilkan input satuan
TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
enabled: !isUang, // Disable jika berbentuk uang
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
@ -561,54 +572,39 @@ class StokBantuanView extends GetView<StokBantuanController> {
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDateTime(tanggalMasuk),
),
// Tambahkan informasi tentang total stok
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
tanggalKadaluarsa = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline,
color: Colors.blue, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
'Informasi',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Total stok akan dihitung otomatis dari jumlah penitipan bantuan yang telah terverifikasi.',
style: TextStyle(fontSize: 12),
),
],
),
),
],
@ -625,12 +621,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
if (formKey.currentState!.validate()) {
final stok = StokBantuanModel(
nama: namaController.text,
totalStok: double.parse(stokController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
isUang: isUang,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
@ -649,15 +643,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController(text: stok.nama);
final stokController =
TextEditingController(text: stok.totalStok?.toString());
final satuanController = TextEditingController(text: stok.satuan);
final deskripsiController = TextEditingController(text: stok.deskripsi);
String? selectedJenisBantuanId = stok.kategoriBantuanId;
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime? tanggalMasuk = stok.tanggalMasuk;
DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa;
bool isUang = stok.isUang ?? false;
showDialog(
context: context,
@ -713,46 +702,55 @@ class StokBantuanView extends GetView<StokBantuanController> {
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: stokController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
// Tambahkan checkbox untuk menandai sebagai uang
CheckboxListTile(
title: const Text('Bantuan Berbentuk Uang (Rupiah)'),
value: isUang,
onChanged: (value) {
setState(() {
isUang = value ?? false;
if (isUang) {
satuanController.text = 'Rp';
}
});
},
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 16),
// Tampilkan total stok saat ini (read-only)
InputDecorator(
decoration: InputDecoration(
labelText: isUang
? 'Total Dana Saat Ini'
: 'Total Stok Saat Ini',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(10),
),
child: Text(
isUang
? 'Rp ${DateFormatter.formatNumber(stok.totalStok)}'
: '${DateFormatter.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 16),
// Hanya tampilkan input satuan
TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
enabled: !isUang, // Disable jika berbentuk uang
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
@ -764,54 +762,39 @@ class StokBantuanView extends GetView<StokBantuanController> {
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDateTime(tanggalMasuk),
),
// Tambahkan informasi tentang total stok
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
tanggalKadaluarsa = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline,
color: Colors.blue, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
'Informasi',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Total stok dihitung otomatis dari jumlah penitipan bantuan yang telah terverifikasi dan tidak dapat diubah secara manual.',
style: TextStyle(fontSize: 12),
),
],
),
),
],
@ -829,12 +812,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
final updatedStok = StokBantuanModel(
id: stok.id,
nama: namaController.text,
totalStok: double.parse(stokController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
isUang: isUang,
createdAt: stok.createdAt,
updatedAt: DateTime.now(),
);

View File

@ -442,6 +442,22 @@ class SupabaseService extends GetxService {
}
}
// Metode untuk mengambil data penitipan bantuan dengan status TERVERIFIKASI
Future<List<Map<String, dynamic>>?> getPenitipanBantuanTerverifikasi() async {
try {
final response = await client
.from('penitipan_bantuan')
.select('*, donatur:donatur_id(*), stok_bantuan:stok_bantuan_id(*)')
.eq('status', 'TERVERIFIKASI')
.order('tanggal_penitipan', ascending: false);
return response;
} catch (e) {
print('Error getting penitipan bantuan terverifikasi: $e');
return null;
}
}
// Upload file methods
Future<String?> uploadFile(
String filePath, String bucket, String folder) async {