From 8d5fb275e8d76d6d2df1214aecc9e22aef5d1d30 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Wed, 12 Mar 2025 08:42:00 +0700 Subject: [PATCH] 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 --- .../data/models/penitipan_bantuan_model.dart | 10 +- lib/app/data/models/stok_bantuan_model.dart | 20 +- .../controllers/stok_bantuan_controller.dart | 173 ++++-- .../petugas_desa/views/penitipan_view.dart | 92 +++- .../petugas_desa/views/stok_bantuan_view.dart | 503 +++++++++--------- lib/app/services/supabase_service.dart | 16 + 6 files changed, 469 insertions(+), 345 deletions(-) diff --git a/lib/app/data/models/penitipan_bantuan_model.dart b/lib/app/data/models/penitipan_bantuan_model.dart index 75f42af..805c95a 100644 --- a/lib/app/data/models/penitipan_bantuan_model.dart +++ b/lib/app/data/models/penitipan_bantuan_model.dart @@ -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 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, }; } diff --git a/lib/app/data/models/stok_bantuan_model.dart b/lib/app/data/models/stok_bantuan_model.dart index 731493c..06721bf 100644 --- a/lib/app/data/models/stok_bantuan_model.dart +++ b/lib/app/data/models/stok_bantuan_model.dart @@ -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 toJson() { final Map 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 diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart index c45bb06..cc65d9c 100644 --- a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -13,9 +13,10 @@ class StokBantuanController extends GetxController { // Data untuk stok bantuan final RxList daftarStokBantuan = [].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> daftarPenitipanTerverifikasi = + >[].obs; // Data untuk kategori bantuan final RxList> 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 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 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 loadKategoriBantuanData() async { try { final kategoriBantuanData = await _supabaseService.getKategoriBantuan(); @@ -86,13 +146,21 @@ class StokBantuanController extends GetxController { } Future 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 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 deleteStok(String stokId) async { - isLoading.value = true; + Future 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 refreshData() async { isLoading.value = true; - try { - await loadStokBantuanData(); - await loadKategoriBantuanData(); - } finally { - isLoading.value = false; - } + await loadStokBantuanData(); + await loadPenitipanTerverifikasi(); + isLoading.value = false; } List 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 _hitungTotalStok() async { + // Implementasi metode _hitungTotalStok + } + + Future _filterStokBantuan() async { + // Implementasi metode _filterStokBantuan } } diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index 52d2d1f..9b81fe3 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -294,6 +294,9 @@ class PenitipanView extends GetView { 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 { 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 { 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 { 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 { 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 { _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 { ], ), + // 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) diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 1ea8f72..3e5e282 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -72,37 +72,15 @@ class StokBantuanView extends GetView { 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 { 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 { 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 { 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 { void _showAddStokDialog(BuildContext context) { final formKey = GlobalKey(); 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 { }, ), 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 { 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 { 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 { void _showEditStokDialog(BuildContext context, StokBantuanModel stok) { final formKey = GlobalKey(); 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 { }, ), 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 { 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 { 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(), ); diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index 3336d1c..edec2a0 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -442,6 +442,22 @@ class SupabaseService extends GetxService { } } + // Metode untuk mengambil data penitipan bantuan dengan status TERVERIFIKASI + Future>?> 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 uploadFile( String filePath, String bucket, String folder) async {