Tambahkan dukungan penyimpanan lokal dan perbaikan manajemen data
- Integrasikan GetStorage untuk menyimpan data counter secara lokal - Tambahkan metode loadCountersFromStorage di CounterService - Perbarui model DonaturModel dan StokBantuanModel untuk konsistensi data - Tambahkan properti lastUpdateTime di controller untuk melacak pembaruan data - Perbaiki tampilan dengan menambahkan informasi waktu terakhir update - Optimalkan metode refresh dan update data di berbagai controller
This commit is contained in:
@ -5,10 +5,8 @@ class DonaturModel {
|
||||
final String? nama;
|
||||
final String? alamat;
|
||||
final String? telepon;
|
||||
final String? noHp;
|
||||
final String? email;
|
||||
final String? jenis;
|
||||
final String? deskripsi;
|
||||
final String? status;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
@ -18,10 +16,8 @@ class DonaturModel {
|
||||
this.nama,
|
||||
this.alamat,
|
||||
this.telepon,
|
||||
this.noHp,
|
||||
this.email,
|
||||
this.jenis,
|
||||
this.deskripsi,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
@ -37,10 +33,8 @@ class DonaturModel {
|
||||
nama: json["nama"],
|
||||
alamat: json["alamat"],
|
||||
telepon: json["telepon"],
|
||||
noHp: json["no_hp"] ?? json["telepon"],
|
||||
email: json["email"],
|
||||
jenis: json["jenis"],
|
||||
deskripsi: json["deskripsi"],
|
||||
status: json["status"] ?? 'AKTIF',
|
||||
createdAt: json["created_at"] != null
|
||||
? DateTime.parse(json["created_at"])
|
||||
@ -55,10 +49,8 @@ class DonaturModel {
|
||||
"nama": nama,
|
||||
"alamat": alamat,
|
||||
"telepon": telepon,
|
||||
"no_hp": noHp ?? telepon,
|
||||
"email": email,
|
||||
"jenis": jenis,
|
||||
"deskripsi": deskripsi,
|
||||
"status": status ?? 'AKTIF',
|
||||
"created_at": createdAt?.toIso8601String(),
|
||||
"updated_at": updatedAt?.toIso8601String(),
|
||||
|
@ -36,7 +36,11 @@ class StokBantuanModel {
|
||||
nama: json["nama"],
|
||||
kategoriBantuanId: json["kategori_bantuan_id"],
|
||||
kategoriBantuan: json["kategori_bantuan"],
|
||||
totalStok: 0.0,
|
||||
totalStok: json["total_stok"] != null
|
||||
? (json["total_stok"] is int
|
||||
? json["total_stok"].toDouble()
|
||||
: json["total_stok"])
|
||||
: 0.0,
|
||||
satuan: json["satuan"],
|
||||
deskripsi: json["deskripsi"],
|
||||
createdAt: json["created_at"] != null
|
||||
@ -64,6 +68,11 @@ class StokBantuanModel {
|
||||
data["id"] = id;
|
||||
}
|
||||
|
||||
// Tambahkan total_stok hanya jika tidak null
|
||||
if (totalStok != null) {
|
||||
data["total_stok"] = totalStok;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,21 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
/// Service untuk berbagi data counter antar controller
|
||||
class CounterService extends GetxService {
|
||||
static CounterService get to => Get.find<CounterService>();
|
||||
|
||||
// Penyimpanan lokal
|
||||
final GetStorage _storage = GetStorage();
|
||||
|
||||
// Keys untuk penyimpanan
|
||||
static const String _keyMenunggu = 'counter_menunggu';
|
||||
static const String _keyTerverifikasi = 'counter_terverifikasi';
|
||||
static const String _keyDitolak = 'counter_ditolak';
|
||||
static const String _keyDiproses = 'counter_diproses';
|
||||
static const String _keyNotifikasi = 'counter_notifikasi';
|
||||
static const String _keyJadwal = 'counter_jadwal';
|
||||
|
||||
// Counter untuk penitipan
|
||||
final RxInt jumlahMenunggu = 0.obs;
|
||||
final RxInt jumlahTerverifikasi = 0.obs;
|
||||
@ -18,6 +30,26 @@ class CounterService extends GetxService {
|
||||
// Counter untuk jadwal
|
||||
final RxInt jumlahJadwalHariIni = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Muat nilai counter dari penyimpanan lokal
|
||||
loadCountersFromStorage();
|
||||
}
|
||||
|
||||
// Metode untuk memuat counter dari penyimpanan lokal
|
||||
void loadCountersFromStorage() {
|
||||
jumlahMenunggu.value = _storage.read(_keyMenunggu) ?? 0;
|
||||
jumlahTerverifikasi.value = _storage.read(_keyTerverifikasi) ?? 0;
|
||||
jumlahDitolak.value = _storage.read(_keyDitolak) ?? 0;
|
||||
jumlahDiproses.value = _storage.read(_keyDiproses) ?? 0;
|
||||
jumlahNotifikasiBelumDibaca.value = _storage.read(_keyNotifikasi) ?? 0;
|
||||
jumlahJadwalHariIni.value = _storage.read(_keyJadwal) ?? 0;
|
||||
|
||||
print(
|
||||
'Counter loaded from storage - Menunggu: ${jumlahMenunggu.value}, Terverifikasi: ${jumlahTerverifikasi.value}, Ditolak: ${jumlahDitolak.value}');
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui counter penitipan
|
||||
void updatePenitipanCounters({
|
||||
required int menunggu,
|
||||
@ -27,20 +59,31 @@ class CounterService extends GetxService {
|
||||
jumlahMenunggu.value = menunggu;
|
||||
jumlahTerverifikasi.value = terverifikasi;
|
||||
jumlahDitolak.value = ditolak;
|
||||
|
||||
// Simpan ke penyimpanan lokal
|
||||
_storage.write(_keyMenunggu, menunggu);
|
||||
_storage.write(_keyTerverifikasi, terverifikasi);
|
||||
_storage.write(_keyDitolak, ditolak);
|
||||
|
||||
print(
|
||||
'Counter updated and saved - Menunggu: $menunggu, Terverifikasi: $terverifikasi, Ditolak: $ditolak');
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui counter pengaduan
|
||||
void updatePengaduanCounter(int diproses) {
|
||||
jumlahDiproses.value = diproses;
|
||||
_storage.write(_keyDiproses, diproses);
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui counter notifikasi
|
||||
void updateNotifikasiCounter(int belumDibaca) {
|
||||
jumlahNotifikasiBelumDibaca.value = belumDibaca;
|
||||
_storage.write(_keyNotifikasi, belumDibaca);
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui counter jadwal
|
||||
void updateJadwalCounter(int hariIni) {
|
||||
jumlahJadwalHariIni.value = hariIni;
|
||||
_storage.write(_keyJadwal, hariIni);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ class PenitipanBantuanController extends GetxController {
|
||||
// Controller untuk pencarian
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
// Tambahkan properti untuk waktu terakhir update
|
||||
Rx<DateTime> lastUpdateTime = DateTime.now().obs;
|
||||
|
||||
UserModel? get user => _authController.user;
|
||||
|
||||
// Getter untuk counter dari CounterService
|
||||
@ -75,6 +78,13 @@ class PenitipanBantuanController extends GetxController {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Pastikan counter diperbarui saat tab diakses kembali
|
||||
updateCounters();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
@ -82,6 +92,13 @@ class PenitipanBantuanController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui data saat tab diakses kembali
|
||||
void onTabReactivated() {
|
||||
print('Penitipan tab reactivated - refreshing data');
|
||||
// Selalu muat ulang data dari server saat tab diaktifkan kembali
|
||||
refreshData();
|
||||
}
|
||||
|
||||
Future<void> loadPenitipanData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
@ -92,20 +109,7 @@ class PenitipanBantuanController extends GetxController {
|
||||
.toList();
|
||||
|
||||
// Hitung jumlah berdasarkan status
|
||||
int menunggu =
|
||||
daftarPenitipan.where((item) => item.status == 'MENUNGGU').length;
|
||||
int terverifikasi = daftarPenitipan
|
||||
.where((item) => item.status == 'TERVERIFIKASI')
|
||||
.length;
|
||||
int ditolak =
|
||||
daftarPenitipan.where((item) => item.status == 'DITOLAK').length;
|
||||
|
||||
// Update counter di CounterService
|
||||
_counterService.updatePenitipanCounters(
|
||||
menunggu: menunggu,
|
||||
terverifikasi: terverifikasi,
|
||||
ditolak: ditolak,
|
||||
);
|
||||
updateCounters();
|
||||
|
||||
// Muat informasi petugas desa untuk item yang terverifikasi
|
||||
print(
|
||||
@ -130,6 +134,9 @@ class PenitipanBantuanController extends GetxController {
|
||||
petugasDesaCache.forEach((key, value) {
|
||||
print('ID: $key, Nama: ${value['name']}');
|
||||
});
|
||||
|
||||
// Update waktu terakhir refresh
|
||||
lastUpdateTime.value = DateTime.now();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading penitipan data: $e');
|
||||
@ -268,6 +275,9 @@ class PenitipanBantuanController extends GetxController {
|
||||
fotoBantuanPaths.clear();
|
||||
|
||||
await loadPenitipanData();
|
||||
// Pastikan counter diperbarui setelah penambahan
|
||||
updateCounters();
|
||||
|
||||
Get.back(); // Tutup dialog
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
@ -313,6 +323,9 @@ class PenitipanBantuanController extends GetxController {
|
||||
fotoBuktiSerahTerimaPath.value = null;
|
||||
|
||||
await loadPenitipanData();
|
||||
// Pastikan counter diperbarui setelah verifikasi
|
||||
updateCounters();
|
||||
|
||||
Get.back(); // Tutup dialog
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
@ -341,6 +354,9 @@ class PenitipanBantuanController extends GetxController {
|
||||
try {
|
||||
await _supabaseService.tolakPenitipan(penitipanId, alasan);
|
||||
await loadPenitipanData();
|
||||
// Pastikan counter diperbarui setelah penolakan
|
||||
updateCounters();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Penitipan berhasil ditolak',
|
||||
@ -414,7 +430,10 @@ class PenitipanBantuanController extends GetxController {
|
||||
|
||||
Future<void> refreshData() async {
|
||||
await loadPenitipanData();
|
||||
await loadKategoriBantuanData();
|
||||
await loadStokBantuanData();
|
||||
|
||||
// Update waktu terakhir refresh
|
||||
lastUpdateTime.value = DateTime.now();
|
||||
}
|
||||
|
||||
void changeCategory(int index) {
|
||||
@ -615,16 +634,19 @@ class PenitipanBantuanController extends GetxController {
|
||||
|
||||
Future<String?> tambahDonatur({
|
||||
required String nama,
|
||||
required String noHp,
|
||||
required String telepon,
|
||||
String? alamat,
|
||||
String? email,
|
||||
String? jenis,
|
||||
}) async {
|
||||
try {
|
||||
final donaturData = {
|
||||
'nama': nama,
|
||||
'no_hp': noHp,
|
||||
'telepon': telepon,
|
||||
'alamat': alamat,
|
||||
'email': email,
|
||||
'jenis': jenis,
|
||||
'status': 'AKTIF',
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
@ -650,4 +672,25 @@ class PenitipanBantuanController extends GetxController {
|
||||
}
|
||||
return stokBantuanMap[stokBantuanId]?.isUang ?? false;
|
||||
}
|
||||
|
||||
// Metode baru untuk memperbarui counter
|
||||
void updateCounters() {
|
||||
int menunggu =
|
||||
daftarPenitipan.where((item) => item.status == 'MENUNGGU').length;
|
||||
int terverifikasi =
|
||||
daftarPenitipan.where((item) => item.status == 'TERVERIFIKASI').length;
|
||||
int ditolak =
|
||||
daftarPenitipan.where((item) => item.status == 'DITOLAK').length;
|
||||
|
||||
// Update counter di CounterService
|
||||
_counterService.updatePenitipanCounters(
|
||||
menunggu: menunggu,
|
||||
terverifikasi: terverifikasi,
|
||||
ditolak: ditolak,
|
||||
);
|
||||
|
||||
// Debug counter values
|
||||
print(
|
||||
'Counter updated - Menunggu: $menunggu, Terverifikasi: $terverifikasi, Ditolak: $ditolak');
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
|
||||
|
||||
class PetugasDesaController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
@ -220,10 +222,28 @@ class PetugasDesaController extends GetxController {
|
||||
|
||||
// Jika tab penitipan dipilih, muat ulang data penitipan
|
||||
if (index == 2) {
|
||||
loadPenitipanData();
|
||||
// Dapatkan instance PenitipanBantuanController dan panggil onTabReactivated
|
||||
try {
|
||||
final penitipanController = Get.find<PenitipanBantuanController>();
|
||||
penitipanController.onTabReactivated();
|
||||
print('Memanggil onTabReactivated pada PenitipanBantuanController');
|
||||
} catch (e) {
|
||||
print('Error saat memanggil onTabReactivated: $e');
|
||||
// Fallback ke metode lama jika controller tidak ditemukan
|
||||
loadPenitipanData();
|
||||
}
|
||||
} else if (index == 3) {
|
||||
// Jika tab pengaduan dipilih, muat ulang data pengaduan
|
||||
loadPengaduanData();
|
||||
} else if (index == 4) {
|
||||
// Jika tab stok bantuan dipilih, muat ulang data stok bantuan
|
||||
try {
|
||||
final stokBantuanController = Get.find<StokBantuanController>();
|
||||
stokBantuanController.onTabReactivated();
|
||||
print('Memanggil onTabReactivated pada StokBantuanController');
|
||||
} catch (e) {
|
||||
print('Error saat memanggil onTabReactivated: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,9 @@ class StokBantuanController extends GetxController {
|
||||
// Tambahkan properti untuk total dana bantuan
|
||||
RxDouble totalDanaBantuan = 0.0.obs;
|
||||
|
||||
// Tambahkan properti untuk waktu terakhir update
|
||||
Rx<DateTime> lastUpdateTime = DateTime.now().obs;
|
||||
|
||||
UserModel? get user => _authController.user;
|
||||
|
||||
@override
|
||||
@ -54,6 +57,12 @@ class StokBantuanController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui data saat tab diaktifkan kembali
|
||||
void onTabReactivated() {
|
||||
print('Stok Bantuan tab reactivated - refreshing data');
|
||||
refreshData();
|
||||
}
|
||||
|
||||
Future<void> loadStokBantuanData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
@ -65,6 +74,9 @@ class StokBantuanController extends GetxController {
|
||||
|
||||
// Hitung total dana bantuan
|
||||
_hitungTotalDanaBantuan();
|
||||
|
||||
// Update waktu terakhir refresh
|
||||
lastUpdateTime.value = DateTime.now();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading stok bantuan data: $e');
|
||||
@ -79,65 +91,14 @@ class StokBantuanController extends GetxController {
|
||||
await _supabaseService.getPenitipanBantuanTerverifikasi();
|
||||
if (penitipanData != null) {
|
||||
daftarPenitipanTerverifikasi.value = penitipanData;
|
||||
// Update total stok berdasarkan penitipan terverifikasi
|
||||
_hitungTotalStokDariPenitipan();
|
||||
// Tidak perlu lagi menghitung total stok dari penitipan
|
||||
// karena total_stok sudah dikelola oleh trigger database
|
||||
}
|
||||
} 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();
|
||||
@ -151,17 +112,14 @@ class StokBantuanController extends GetxController {
|
||||
|
||||
Future<void> addStok(StokBantuanModel stok) async {
|
||||
try {
|
||||
// Buat data stok baru tanpa field total_stok
|
||||
// Buat data stok baru
|
||||
final stokData = stok.toJson();
|
||||
|
||||
// Hapus field total_stok dari data yang akan dikirim ke database
|
||||
if (stokData.containsKey('total_stok')) {
|
||||
stokData.remove('total_stok');
|
||||
}
|
||||
// Tambahkan total_stok = 0 untuk stok baru
|
||||
stokData['total_stok'] = 0.0;
|
||||
|
||||
await _supabaseService.addStok(stokData);
|
||||
await loadStokBantuanData();
|
||||
await loadPenitipanTerverifikasi();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Stok bantuan berhasil ditambahkan',
|
||||
@ -187,13 +145,13 @@ class StokBantuanController extends GetxController {
|
||||
final stokData = stok.toJson();
|
||||
|
||||
// Hapus field total_stok dari data yang akan dikirim ke database
|
||||
// karena total_stok dikelola oleh trigger database
|
||||
if (stokData.containsKey('total_stok')) {
|
||||
stokData.remove('total_stok');
|
||||
}
|
||||
|
||||
await _supabaseService.updateStok(stok.id ?? '', stokData);
|
||||
await loadStokBantuanData();
|
||||
await loadPenitipanTerverifikasi();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Stok bantuan berhasil diperbarui',
|
||||
@ -217,7 +175,6 @@ class StokBantuanController extends GetxController {
|
||||
try {
|
||||
await _supabaseService.deleteStok(id);
|
||||
await loadStokBantuanData(); // Ini akan memanggil _hitungTotalDanaBantuan()
|
||||
await loadPenitipanTerverifikasi(); // Perbarui data penitipan terverifikasi
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Stok bantuan berhasil dihapus',
|
||||
@ -241,6 +198,10 @@ class StokBantuanController extends GetxController {
|
||||
isLoading.value = true;
|
||||
await loadStokBantuanData();
|
||||
await loadPenitipanTerverifikasi();
|
||||
|
||||
// Update waktu terakhir refresh
|
||||
lastUpdateTime.value = DateTime.now();
|
||||
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
@ -306,14 +267,6 @@ class StokBantuanController extends GetxController {
|
||||
totalDanaBantuan.value = total;
|
||||
}
|
||||
|
||||
Future<void> _hitungTotalStok() async {
|
||||
// Implementasi metode _hitungTotalStok
|
||||
}
|
||||
|
||||
Future<void> _filterStokBantuan() async {
|
||||
// Implementasi metode _filterStokBantuan
|
||||
}
|
||||
|
||||
// Metode untuk mengatur filter
|
||||
void setFilter(String value) {
|
||||
filterValue.value = value;
|
||||
|
@ -31,6 +31,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
// Filter dan pencarian
|
||||
_buildFilterSearch(context),
|
||||
|
||||
// Informasi terakhir update
|
||||
_buildLastUpdateInfo(context),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Daftar penitipan
|
||||
@ -43,7 +46,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showTambahPenitipanDialog(context),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
child: const Icon(Icons.add),
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -388,25 +391,32 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildItemDetail(
|
||||
context,
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Tanggal Penitipan',
|
||||
value: DateFormatter.formatDateTime(item.tanggalPenitipan,
|
||||
defaultValue: 'Tidak ada tanggal'),
|
||||
),
|
||||
|
||||
// Tampilkan informasi petugas desa jika status terverifikasi
|
||||
if (item.status == 'TERVERIFIKASI' &&
|
||||
item.petugasDesaId != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildItemDetail(
|
||||
context,
|
||||
icon: Icons.person,
|
||||
label: 'Diverifikasi Oleh',
|
||||
value: controller.getPetugasDesaNama(item.petugasDesaId),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildItemDetail(
|
||||
context,
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Tanggal Dibuat',
|
||||
value: DateFormatter.formatDateTime(item.createdAt,
|
||||
defaultValue: 'Tidak ada tanggal'),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: item.status == 'TERVERIFIKASI' &&
|
||||
item.petugasDesaId != null
|
||||
? _buildItemDetail(
|
||||
context,
|
||||
icon: Icons.person,
|
||||
label: 'Diverifikasi Oleh',
|
||||
value:
|
||||
controller.getPetugasDesaNama(item.petugasDesaId),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Tampilkan thumbnail foto bantuan jika ada
|
||||
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty)
|
||||
@ -721,8 +731,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
'Diverifikasi Oleh',
|
||||
controller.getPetugasDesaNama(item.petugasDesaId),
|
||||
),
|
||||
_buildDetailItem('Tanggal Masuk',
|
||||
DateFormatter.formatDateTime(item.tanggalPenitipan)),
|
||||
_buildDetailItem('Tanggal Dibuat',
|
||||
DateFormatter.formatDateTime(item.createdAt)),
|
||||
if (item.alasanPenolakan != null &&
|
||||
item.alasanPenolakan!.isNotEmpty)
|
||||
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
|
||||
@ -1029,7 +1039,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Penitipan Bantuan',
|
||||
'Tambah Manual Penitipan Bantuan',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@ -1163,8 +1173,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (selectedDonatur.value!.noHp != null)
|
||||
Text(selectedDonatur.value!.noHp!),
|
||||
if (selectedDonatur.value!.telepon !=
|
||||
null)
|
||||
Text(selectedDonatur.value!.telepon!),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -1228,8 +1239,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
return ListTile(
|
||||
title:
|
||||
Text(donatur.nama ?? 'Tidak ada nama'),
|
||||
subtitle: donatur.noHp != null
|
||||
? Text(donatur.noHp!)
|
||||
subtitle: donatur.telepon != null
|
||||
? Text(donatur.telepon!)
|
||||
: null,
|
||||
dense: true,
|
||||
onTap: () {
|
||||
@ -1269,6 +1280,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Donatur Baru'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
foregroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
@ -1542,9 +1555,10 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
BuildContext context, Function(String) onDonaturAdded) {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final TextEditingController namaController = TextEditingController();
|
||||
final TextEditingController noHpController = TextEditingController();
|
||||
final TextEditingController teleponController = TextEditingController();
|
||||
final TextEditingController alamatController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController jenisController = TextEditingController();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
@ -1591,32 +1605,80 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// No HP
|
||||
// Telepon
|
||||
Text(
|
||||
'Nomor HP',
|
||||
'Nomor Telepon',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: noHpController,
|
||||
controller: teleponController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan nomor HP',
|
||||
hintText: 'Masukkan nomor telepon',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nomor HP harus diisi';
|
||||
return 'Nomor telepon harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Jenis (opsional)
|
||||
Text(
|
||||
'Jenis Donatur (Opsional)',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
hint: const Text('Pilih jenis donatur'),
|
||||
value: jenisController.text.isEmpty
|
||||
? null
|
||||
: jenisController.text,
|
||||
items: const [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Perorangan',
|
||||
child: Text('Perorangan'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Perusahaan',
|
||||
child: Text('Perusahaan'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Lembaga',
|
||||
child: Text('Lembaga'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Komunitas',
|
||||
child: Text('Komunitas'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Lainnya',
|
||||
child: Text('Lainnya'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
jenisController.text = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Alamat (opsional)
|
||||
Text(
|
||||
'Alamat (Opsional)',
|
||||
@ -1671,13 +1733,16 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final donaturId = await controller.tambahDonatur(
|
||||
nama: namaController.text,
|
||||
noHp: noHpController.text,
|
||||
telepon: teleponController.text,
|
||||
alamat: alamatController.text.isEmpty
|
||||
? null
|
||||
: alamatController.text,
|
||||
email: emailController.text.isEmpty
|
||||
? null
|
||||
: emailController.text,
|
||||
jenis: jenisController.text.isEmpty
|
||||
? null
|
||||
: jenisController.text,
|
||||
);
|
||||
|
||||
if (donaturId != null) {
|
||||
@ -1708,4 +1773,30 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
return Obx(() {
|
||||
final lastUpdate = controller.lastUpdateTime.value;
|
||||
final formattedDate = DateFormatter.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.update, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Data terupdate: $formattedDate',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
// Filter dan pencarian
|
||||
_buildFilterSearch(context),
|
||||
|
||||
// Informasi terakhir update
|
||||
_buildLastUpdateInfo(context),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Daftar stok bantuan
|
||||
@ -74,7 +77,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Data stok diambil dari penitipan bantuan terverifikasi',
|
||||
'Total stok diperbarui otomatis saat ada penitipan bantuan terverifikasi',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
@ -508,164 +511,229 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
String? selectedJenisBantuanId;
|
||||
bool isUang = false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Tambah Stok Bantuan'),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: namaController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Bantuan',
|
||||
border: OutlineInputBorder(),
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) => Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Stok Bantuan',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama bantuan tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kategori Bantuan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
value: selectedJenisBantuanId,
|
||||
hint: const Text('Pilih Kategori Bantuan'),
|
||||
items: controller.daftarKategoriBantuan
|
||||
.map((kategori) => DropdownMenuItem<String>(
|
||||
value: kategori['id'],
|
||||
child: Text(kategori['nama'] ?? ''),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
selectedJenisBantuanId = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kategori bantuan harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 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 = '';
|
||||
// Nama Bantuan
|
||||
Text(
|
||||
'Nama Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: namaController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan nama bantuan',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama bantuan tidak boleh kosong';
|
||||
}
|
||||
});
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Hapus input jumlah/stok dan hanya tampilkan input satuan
|
||||
TextFormField(
|
||||
controller: satuanController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Satuan',
|
||||
border: OutlineInputBorder(),
|
||||
// Kategori Bantuan
|
||||
Text(
|
||||
'Kategori Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
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(
|
||||
controller: deskripsiController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Deskripsi',
|
||||
border: OutlineInputBorder(),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
hint: const Text('Pilih kategori bantuan'),
|
||||
value: selectedJenisBantuanId,
|
||||
items: controller.daftarKategoriBantuan
|
||||
.map((kategori) => DropdownMenuItem<String>(
|
||||
value: kategori['id'],
|
||||
child: Text(kategori['nama'] ?? ''),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
selectedJenisBantuanId = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kategori bantuan harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 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),
|
||||
|
||||
// Checkbox untuk bantuan berbentuk 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,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
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: 16),
|
||||
|
||||
// Satuan
|
||||
Text(
|
||||
'Satuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: satuanController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Contoh: Kg, Liter, Paket',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
enabled: !isUang, // Disable jika berbentuk uang
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Satuan tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Deskripsi
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: deskripsiController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan deskripsi bantuan',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Informasi
|
||||
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)),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Total stok akan dihitung otomatis dari jumlah penitipan bantuan yang telah terverifikasi.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final stok = StokBantuanModel(
|
||||
nama: namaController.text,
|
||||
satuan: satuanController.text,
|
||||
deskripsi: deskripsiController.text,
|
||||
kategoriBantuanId: selectedJenisBantuanId,
|
||||
isUang: isUang,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
controller.addStok(stok);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final stok = StokBantuanModel(
|
||||
nama: namaController.text,
|
||||
satuan: satuanController.text,
|
||||
deskripsi: deskripsiController.text,
|
||||
kategoriBantuanId: selectedJenisBantuanId,
|
||||
isUang: isUang,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
controller.addStok(stok);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
@ -677,211 +745,409 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
String? selectedJenisBantuanId = stok.kategoriBantuanId;
|
||||
bool isUang = stok.isUang ?? false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Edit Stok Bantuan'),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: namaController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nama Bantuan',
|
||||
border: OutlineInputBorder(),
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) => Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Edit Stok Bantuan',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama bantuan tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kategori Bantuan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
value: selectedJenisBantuanId,
|
||||
hint: const Text('Pilih Kategori Bantuan'),
|
||||
isExpanded: true,
|
||||
items: controller.daftarKategoriBantuan
|
||||
.map((kategori) => DropdownMenuItem<String>(
|
||||
value: kategori['id'],
|
||||
child: Text(
|
||||
kategori['nama'] ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
selectedJenisBantuanId = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kategori bantuan harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 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';
|
||||
// Nama Bantuan
|
||||
Text(
|
||||
'Nama Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: namaController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan nama bantuan',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama bantuan tidak boleh kosong';
|
||||
}
|
||||
});
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
return null;
|
||||
},
|
||||
),
|
||||
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),
|
||||
// Kategori Bantuan
|
||||
Text(
|
||||
'Kategori Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
child: Text(
|
||||
isUang
|
||||
? 'Rp ${DateFormatter.formatNumber(stok.totalStok)}'
|
||||
: '${DateFormatter.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
hint: const Text('Pilih kategori bantuan'),
|
||||
value: selectedJenisBantuanId,
|
||||
isExpanded: true,
|
||||
items: controller.daftarKategoriBantuan
|
||||
.map((kategori) => DropdownMenuItem<String>(
|
||||
value: kategori['id'],
|
||||
child: Text(
|
||||
kategori['nama'] ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
selectedJenisBantuanId = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kategori bantuan harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Hanya tampilkan input satuan
|
||||
TextFormField(
|
||||
controller: satuanController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Satuan',
|
||||
border: OutlineInputBorder(),
|
||||
// Checkbox untuk bantuan berbentuk 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,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
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(
|
||||
controller: deskripsiController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Deskripsi',
|
||||
border: OutlineInputBorder(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Total Stok Saat Ini
|
||||
Text(
|
||||
isUang ? 'Total Dana Saat Ini' : 'Total Stok Saat Ini',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 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: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isUang ? Icons.monetization_on : Icons.inventory_2,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isUang
|
||||
? 'Rp ${DateFormatter.formatNumber(stok.totalStok)}'
|
||||
: '${DateFormatter.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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: 16),
|
||||
|
||||
// Satuan
|
||||
Text(
|
||||
'Satuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: satuanController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Contoh: Kg, Liter, Paket',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
enabled: !isUang, // Disable jika berbentuk uang
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Satuan tidak boleh kosong';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Deskripsi
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: deskripsiController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan deskripsi bantuan',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Informasi
|
||||
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)),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
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),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final updatedStok = StokBantuanModel(
|
||||
id: stok.id,
|
||||
nama: namaController.text,
|
||||
satuan: satuanController.text,
|
||||
deskripsi: deskripsiController.text,
|
||||
kategoriBantuanId: selectedJenisBantuanId,
|
||||
isUang: isUang,
|
||||
createdAt: stok.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
controller.updateStok(updatedStok);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final updatedStok = StokBantuanModel(
|
||||
id: stok.id,
|
||||
nama: namaController.text,
|
||||
satuan: satuanController.text,
|
||||
deskripsi: deskripsiController.text,
|
||||
kategoriBantuanId: selectedJenisBantuanId,
|
||||
isUang: isUang,
|
||||
createdAt: stok.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
controller.updateStok(updatedStok);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context, StokBantuanModel stok) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Konfirmasi Hapus'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus stok bantuan "${stok.nama}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Konfirmasi Hapus',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Apakah Anda yakin ingin menghapus stok bantuan berikut?'),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
stok.nama ?? 'Tanpa Nama',
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
if (stok.deskripsi != null &&
|
||||
stok.deskripsi!.isNotEmpty) ...[
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
stok.deskripsi!,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
stok.isUang == true
|
||||
? Icons.monetization_on
|
||||
: Icons.inventory,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
stok.isUang == true
|
||||
? 'Rp ${DateFormatter.formatNumber(stok.totalStok)}'
|
||||
: '${DateFormatter.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
color: Colors.red, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Perhatian: Tindakan ini tidak dapat dibatalkan!',
|
||||
style: TextStyle(
|
||||
color: Colors.red, fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.deleteStok(stok.id ?? '');
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.deleteStok(stok.id ?? '');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
return Obx(() {
|
||||
final lastUpdate = controller.lastUpdateTime.value;
|
||||
final formattedDate = DateFormatter.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.update, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Data terupdate: $formattedDate',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,8 @@ class DateFormatter {
|
||||
return number.toString(); // Fallback to basic format
|
||||
}
|
||||
}
|
||||
|
||||
static String formatDateTimeWithHour(DateTime dateTime) {
|
||||
return DateFormat('dd MMMM yyyy HH:mm', 'id_ID').format(dateTime);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user