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

View File

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

View File

@ -13,9 +13,10 @@ class StokBantuanController extends GetxController {
// Data untuk stok bantuan // Data untuk stok bantuan
final RxList<StokBantuanModel> daftarStokBantuan = <StokBantuanModel>[].obs; final RxList<StokBantuanModel> daftarStokBantuan = <StokBantuanModel>[].obs;
final RxDouble totalStok = 0.0.obs;
final RxDouble stokMasuk = 0.0.obs; // Data untuk penitipan bantuan terverifikasi
final RxDouble stokKeluar = 0.0.obs; final RxList<Map<String, dynamic>> daftarPenitipanTerverifikasi =
<Map<String, dynamic>>[].obs;
// Data untuk kategori bantuan // Data untuk kategori bantuan
final RxList<Map<String, dynamic>> daftarKategoriBantuan = final RxList<Map<String, dynamic>> daftarKategoriBantuan =
@ -25,6 +26,9 @@ class StokBantuanController extends GetxController {
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
final RxString searchQuery = ''.obs; final RxString searchQuery = ''.obs;
// Tambahkan properti untuk total dana bantuan
RxDouble totalDanaBantuan = 0.0.obs;
UserModel? get user => _authController.user; UserModel? get user => _authController.user;
@override @override
@ -32,6 +36,7 @@ class StokBantuanController extends GetxController {
super.onInit(); super.onInit();
loadStokBantuanData(); loadStokBantuanData();
loadKategoriBantuanData(); loadKategoriBantuanData();
loadPenitipanTerverifikasi();
// Listener untuk pencarian // Listener untuk pencarian
searchController.addListener(() { searchController.addListener(() {
@ -54,18 +59,8 @@ class StokBantuanController extends GetxController {
.map((data) => StokBantuanModel.fromJson(data)) .map((data) => StokBantuanModel.fromJson(data))
.toList(); .toList();
// Hitung total stok // Hitung total dana bantuan
totalStok.value = 0; _hitungTotalDanaBantuan();
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;
}
} }
} catch (e) { } catch (e) {
print('Error loading stok bantuan data: $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 { Future<void> loadKategoriBantuanData() async {
try { try {
final kategoriBantuanData = await _supabaseService.getKategoriBantuan(); final kategoriBantuanData = await _supabaseService.getKategoriBantuan();
@ -86,13 +146,21 @@ class StokBantuanController extends GetxController {
} }
Future<void> addStok(StokBantuanModel stok) async { Future<void> addStok(StokBantuanModel stok) async {
isLoading.value = true;
try { 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 loadStokBantuanData();
await loadPenitipanTerverifikasi();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Stok berhasil ditambahkan', 'Stok bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
@ -101,24 +169,30 @@ class StokBantuanController extends GetxController {
print('Error adding stok: $e'); print('Error adding stok: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menambahkan stok: ${e.toString()}', 'Gagal menambahkan stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
} finally {
isLoading.value = false;
} }
} }
Future<void> updateStok(StokBantuanModel stok) async { Future<void> updateStok(StokBantuanModel stok) async {
isLoading.value = true;
try { 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 loadStokBantuanData();
await loadPenitipanTerverifikasi();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Stok berhasil diperbarui', 'Stok bantuan berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
@ -127,24 +201,22 @@ class StokBantuanController extends GetxController {
print('Error updating stok: $e'); print('Error updating stok: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memperbarui stok: ${e.toString()}', 'Gagal memperbarui stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
} finally {
isLoading.value = false;
} }
} }
Future<void> deleteStok(String stokId) async { Future<void> deleteStok(String id) async {
isLoading.value = true;
try { try {
await _supabaseService.deleteStok(stokId); await _supabaseService.deleteStok(id);
await loadStokBantuanData(); await loadStokBantuanData(); // Ini akan memanggil _hitungTotalDanaBantuan()
await loadPenitipanTerverifikasi(); // Perbarui data penitipan terverifikasi
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Stok berhasil dihapus', 'Stok bantuan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
@ -153,25 +225,20 @@ class StokBantuanController extends GetxController {
print('Error deleting stok: $e'); print('Error deleting stok: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menghapus stok: ${e.toString()}', 'Gagal menghapus stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
} finally {
isLoading.value = false;
} }
} }
Future<void> refreshData() async { Future<void> refreshData() async {
isLoading.value = true; isLoading.value = true;
try {
await loadStokBantuanData(); await loadStokBantuanData();
await loadKategoriBantuanData(); await loadPenitipanTerverifikasi();
} finally {
isLoading.value = false; isLoading.value = false;
} }
}
List<StokBantuanModel> getFilteredStokBantuan() { List<StokBantuanModel> getFilteredStokBantuan() {
if (searchQuery.isEmpty) { if (searchQuery.isEmpty) {
@ -202,12 +269,22 @@ class StokBantuanController extends GetxController {
.length; .length;
} }
// Metode untuk mendapatkan jumlah stok yang segera kadaluarsa (dalam 30 hari) // Metode untuk menghitung total dana bantuan
int getStokSegeraKadaluarsa() { void _hitungTotalDanaBantuan() {
return daftarStokBantuan double total = 0.0;
.where((stok) => for (var stok in daftarStokBantuan) {
stok.tanggalKadaluarsa != null && if (stok.isUang == true) {
stok.tanggalKadaluarsa!.difference(DateTime.now()).inDays <= 30) total += stok.totalStok ?? 0.0;
.length; }
}
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( print(
'PenitipanItem - kategoriNama: $kategoriNama, kategoriSatuan: $kategoriSatuan'); 'PenitipanItem - kategoriNama: $kategoriNama, kategoriSatuan: $kategoriSatuan');
// Cek apakah penitipan berbentuk uang
final isUang = item.isUang ?? false;
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
@ -360,7 +363,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Expanded( Expanded(
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.category, icon: isUang ? Icons.monetization_on : Icons.category,
label: 'Kategori Bantuan', label: 'Kategori Bantuan',
value: kategoriNama, value: kategoriNama,
), ),
@ -368,10 +371,12 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Expanded( Expanded(
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.inventory, icon:
isUang ? Icons.account_balance_wallet : Icons.inventory,
label: 'Jumlah', label: 'Jumlah',
value: value: isUang
'${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}', ? 'Rp ${DateFormatter.formatNumber(item.jumlah)}'
: '${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}',
), ),
), ),
], ],
@ -381,7 +386,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context, context,
icon: Icons.calendar_today, icon: Icons.calendar_today,
label: 'Tanggal Penitipan', label: 'Tanggal Penitipan',
value: DateFormatter.formatDate(item.tanggalPenitipan, value: DateFormatter.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'), defaultValue: 'Tidak ada tanggal'),
), ),
@ -673,6 +678,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
final kategoriSatuan = item.kategoriBantuan?.satuan ?? final kategoriSatuan = item.kategoriBantuan?.satuan ??
controller.getKategoriSatuan(item.stokBantuanId); controller.getKategoriSatuan(item.stokBantuanId);
// Cek apakah penitipan berbentuk uang
final isUang = item.isUang ?? false;
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('Detail Penitipan'), title: const Text('Detail Penitipan'),
@ -684,36 +692,39 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
_buildDetailItem('Donatur', donaturNama), _buildDetailItem('Donatur', donaturNama),
_buildDetailItem('Status', item.status ?? 'Tidak diketahui'), _buildDetailItem('Status', item.status ?? 'Tidak diketahui'),
_buildDetailItem('Kategori Bantuan', kategoriNama), _buildDetailItem('Kategori Bantuan', kategoriNama),
_buildDetailItem('Jumlah', _buildDetailItem(
'${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}'), 'Jumlah',
isUang
? 'Rp ${DateFormatter.formatNumber(item.jumlah)}'
: '${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}'),
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
_buildDetailItem( _buildDetailItem(
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'), 'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
_buildDetailItem( _buildDetailItem(
'Tanggal Penitipan', 'Tanggal Penitipan',
DateFormatter.formatDate(item.tanggalPenitipan, DateFormatter.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'), defaultValue: 'Tidak ada tanggal'),
), ),
if (item.tanggalVerifikasi != null) if (item.tanggalVerifikasi != null)
_buildDetailItem( _buildDetailItem(
'Tanggal Verifikasi', 'Tanggal Verifikasi',
DateFormatter.formatDate(item.tanggalVerifikasi), DateFormatter.formatDateTime(item.tanggalVerifikasi),
), ),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem( _buildDetailItem(
'Diverifikasi Oleh', 'Diverifikasi Oleh',
controller.getPetugasDesaNama(item.petugasDesaId), controller.getPetugasDesaNama(item.petugasDesaId),
), ),
if (item.tanggalKadaluarsa != null) _buildDetailItem('Tanggal Masuk',
_buildDetailItem( DateFormatter.formatDateTime(item.tanggalPenitipan)),
'Tanggal Kadaluarsa',
DateFormatter.formatDate(item.tanggalKadaluarsa),
),
if (item.alasanPenolakan != null && if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty) item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!), _buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
// Foto Bantuan // Foto Bantuan
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty) if (!isUang &&
item.fotoBantuan != null &&
item.fotoBantuan!.isNotEmpty)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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 // Bukti Serah Terima
if (item.fotoBuktiSerahTerima != null && if (item.fotoBuktiSerahTerima != null &&
item.fotoBuktiSerahTerima!.isNotEmpty) item.fotoBuktiSerahTerima!.isNotEmpty)

View File

@ -72,37 +72,15 @@ class StokBantuanView extends GetView<StokBantuanController> {
color: Colors.white, color: Colors.white,
), ),
), ),
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),
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Stok Tersedia',
value: DateFormatter.formatNumber(controller.totalStok.value),
),
),
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),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -119,24 +97,74 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded( Expanded(
child: _buildSummaryItem( child: _buildSummaryItem(
context, context,
icon: Icons.access_time, icon: Icons.handshake_outlined,
title: 'Segera Kadaluarsa', title: 'Penitipan',
value: '${controller.getStokSegeraKadaluarsa()}', value: '${controller.daftarPenitipanTerverifikasi.length}',
valueColor: controller.getStokSegeraKadaluarsa() > 0 valueColor: Colors.white,
? Colors.amber
: Colors.white,
), ),
), ),
Expanded( Expanded(
child: _buildSummaryItem( child: _buildSummaryItem(
context, context,
icon: Icons.category_outlined, icon: Icons.inventory_2,
title: 'Kategori Bantuan', title: 'Jenis Bantuan',
value: '${controller.daftarKategoriBantuan.length}', 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,7 +339,17 @@ class StokBantuanView extends GetView<StokBantuanController> {
color: AppTheme.primaryColor.withOpacity(0.1), color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.isUang == true)
const Icon(
Icons.monetization_on,
size: 16,
color: AppTheme.primaryColor,
),
if (item.isUang == true) const SizedBox(width: 4),
Text(
item.kategoriBantuan != null item.kategoriBantuan != null
? (item.kategoriBantuan!['nama'] ?? ? (item.kategoriBantuan!['nama'] ??
'Tidak Ada Kategori') 'Tidak Ada Kategori')
@ -321,6 +359,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
],
),
), ),
], ],
), ),
@ -340,31 +380,13 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded( Expanded(
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.inventory, icon: item.isUang == true
label: 'Total Stok', ? Icons.monetization_on
value: : Icons.inventory,
'${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}', label: item.isUang == true ? 'Total Dana' : 'Total Stok',
), value: item.isUang == true
), ? 'Rp ${DateFormatter.formatNumber(item.totalStok)}'
Expanded( : '${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
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),
), ),
), ),
Expanded( Expanded(
@ -452,14 +474,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
void _showAddStokDialog(BuildContext context) { void _showAddStokDialog(BuildContext context) {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final namaController = TextEditingController(); final namaController = TextEditingController();
final stokController = TextEditingController();
final satuanController = TextEditingController(); final satuanController = TextEditingController();
final deskripsiController = TextEditingController(); final deskripsiController = TextEditingController();
String? selectedJenisBantuanId; String? selectedJenisBantuanId;
bool isUang = false;
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime tanggalMasuk = DateTime.now();
DateTime? tanggalKadaluarsa;
showDialog( showDialog(
context: context, context: context,
@ -510,37 +528,33 @@ class StokBantuanView extends GetView<StokBantuanController> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row(
children: [ // Tambahkan checkbox untuk menandai sebagai uang
Expanded( CheckboxListTile(
flex: 2, title: const Text('Bantuan Berbentuk Uang (Rupiah)'),
child: TextFormField( value: isUang,
controller: stokController, onChanged: (value) {
decoration: const InputDecoration( setState(() {
labelText: 'Jumlah', isUang = value ?? false;
border: OutlineInputBorder(), if (isUang) {
), satuanController.text = 'Rp';
keyboardType: TextInputType.number, } else {
validator: (value) { satuanController.text = '';
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
} }
if (double.tryParse(value) == null) { });
return 'Jumlah harus berupa angka';
}
return null;
}, },
controlAffinity: ListTileControlAffinity.leading,
), ),
), const SizedBox(height: 16),
const SizedBox(width: 8),
Expanded( // Hapus input jumlah/stok dan hanya tampilkan input satuan
flex: 1, TextFormField(
child: TextFormField(
controller: satuanController, controller: satuanController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Satuan', labelText: 'Satuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
enabled: !isUang, // Disable jika berbentuk uang
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong'; return 'Satuan tidak boleh kosong';
@ -548,9 +562,6 @@ class StokBantuanView extends GetView<StokBantuanController> {
return null; return null;
}, },
), ),
),
],
),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: deskripsiController, controller: deskripsiController,
@ -561,54 +572,39 @@ class StokBantuanView extends GetView<StokBantuanController> {
maxLines: 3, maxLines: 3,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
InkWell( // Tambahkan informasi tentang total stok
onTap: () async { Container(
final picked = await showDatePicker( padding: const EdgeInsets.all(12),
context: context, decoration: BoxDecoration(
initialDate: tanggalMasuk, color: Colors.blue.withOpacity(0.1),
firstDate: DateTime(2020), borderRadius: BorderRadius.circular(8),
lastDate: DateTime(2030), border: Border.all(color: Colors.blue.withOpacity(0.3)),
);
if (picked != null) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
), ),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline,
color: Colors.blue, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text( child: Text(
DateFormatter.formatDateTime(tanggalMasuk), 'Informasi',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
), ),
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( const SizedBox(height: 8),
DateFormatter.formatDate(tanggalKadaluarsa), 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()) { if (formKey.currentState!.validate()) {
final stok = StokBantuanModel( final stok = StokBantuanModel(
nama: namaController.text, nama: namaController.text,
totalStok: double.parse(stokController.text),
satuan: satuanController.text, satuan: satuanController.text,
deskripsi: deskripsiController.text, deskripsi: deskripsiController.text,
kategoriBantuanId: selectedJenisBantuanId, kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk, isUang: isUang,
tanggalKadaluarsa: tanggalKadaluarsa,
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
@ -649,15 +643,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) { void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final namaController = TextEditingController(text: stok.nama); final namaController = TextEditingController(text: stok.nama);
final stokController =
TextEditingController(text: stok.totalStok?.toString());
final satuanController = TextEditingController(text: stok.satuan); final satuanController = TextEditingController(text: stok.satuan);
final deskripsiController = TextEditingController(text: stok.deskripsi); final deskripsiController = TextEditingController(text: stok.deskripsi);
String? selectedJenisBantuanId = stok.kategoriBantuanId; String? selectedJenisBantuanId = stok.kategoriBantuanId;
bool isUang = stok.isUang ?? false;
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime? tanggalMasuk = stok.tanggalMasuk;
DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa;
showDialog( showDialog(
context: context, context: context,
@ -713,37 +702,49 @@ class StokBantuanView extends GetView<StokBantuanController> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row(
children: [ // Tambahkan checkbox untuk menandai sebagai uang
Expanded( CheckboxListTile(
flex: 2, title: const Text('Bantuan Berbentuk Uang (Rupiah)'),
child: TextFormField( value: isUang,
controller: stokController, onChanged: (value) {
decoration: const InputDecoration( setState(() {
labelText: 'Jumlah', isUang = value ?? false;
border: OutlineInputBorder(), if (isUang) {
), satuanController.text = 'Rp';
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;
}, },
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(width: 8), const SizedBox(height: 16),
Expanded(
flex: 1, // Hanya tampilkan input satuan
child: TextFormField( TextFormField(
controller: satuanController, controller: satuanController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Satuan', labelText: 'Satuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
enabled: !isUang, // Disable jika berbentuk uang
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong'; return 'Satuan tidak boleh kosong';
@ -751,9 +752,6 @@ class StokBantuanView extends GetView<StokBantuanController> {
return null; return null;
}, },
), ),
),
],
),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: deskripsiController, controller: deskripsiController,
@ -764,54 +762,39 @@ class StokBantuanView extends GetView<StokBantuanController> {
maxLines: 3, maxLines: 3,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
InkWell( // Tambahkan informasi tentang total stok
onTap: () async { Container(
final picked = await showDatePicker( padding: const EdgeInsets.all(12),
context: context, decoration: BoxDecoration(
initialDate: tanggalMasuk ?? DateTime.now(), color: Colors.blue.withOpacity(0.1),
firstDate: DateTime(2020), borderRadius: BorderRadius.circular(8),
lastDate: DateTime(2030), border: Border.all(color: Colors.blue.withOpacity(0.3)),
);
if (picked != null) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
), ),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline,
color: Colors.blue, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text( child: Text(
DateFormatter.formatDateTime(tanggalMasuk), 'Informasi',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
), ),
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( const SizedBox(height: 8),
DateFormatter.formatDate(tanggalKadaluarsa), 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( final updatedStok = StokBantuanModel(
id: stok.id, id: stok.id,
nama: namaController.text, nama: namaController.text,
totalStok: double.parse(stokController.text),
satuan: satuanController.text, satuan: satuanController.text,
deskripsi: deskripsiController.text, deskripsi: deskripsiController.text,
kategoriBantuanId: selectedJenisBantuanId, kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk, isUang: isUang,
tanggalKadaluarsa: tanggalKadaluarsa,
createdAt: stok.createdAt, createdAt: stok.createdAt,
updatedAt: DateTime.now(), 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 // Upload file methods
Future<String?> uploadFile( Future<String?> uploadFile(
String filePath, String bucket, String folder) async { String filePath, String bucket, String folder) async {