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:
Khafidh Fuadi
2025-03-12 15:21:16 +07:00
parent d97c324ac9
commit add585fe23
12 changed files with 882 additions and 448 deletions

View File

@ -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(),

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}
}

View File

@ -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;

View File

@ -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,
),
),
],
),
);
});
}
}

View File

@ -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,
),
),
],
),
);
});
}
}

View File

@ -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);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/services/auth_service.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
@ -10,6 +11,9 @@ import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi GetStorage
await GetStorage.init();
// Inisialisasi data locale untuk format tanggal
await initializeDateFormatting('id_ID', null);

View File

@ -296,6 +296,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.7.2"
get_storage:
dependency: "direct main"
description:
name: get_storage
sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
google_fonts:
dependency: "direct main"
description:

View File

@ -43,6 +43,7 @@ dependencies:
# Untuk menyimpan data lokal
shared_preferences: ^2.2.2
get_storage: ^2.1.1
# Untuk validasi form
form_validator: ^2.1.1