From add585fe2324c4c154d96082133edce473867b9f Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Wed, 12 Mar 2025 15:21:16 +0700 Subject: [PATCH] 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 --- lib/app/data/models/donatur_model.dart | 8 - lib/app/data/models/stok_bantuan_model.dart | 11 +- .../controllers/counter_service.dart | 43 + .../penitipan_bantuan_controller.dart | 77 +- .../controllers/petugas_desa_controller.dart | 22 +- .../controllers/stok_bantuan_controller.dart | 91 +- .../petugas_desa/views/penitipan_view.dart | 157 ++- .../petugas_desa/views/stok_bantuan_view.dart | 904 ++++++++++++------ lib/app/utils/date_formatter.dart | 4 + lib/main.dart | 4 + pubspec.lock | 8 + pubspec.yaml | 1 + 12 files changed, 882 insertions(+), 448 deletions(-) diff --git a/lib/app/data/models/donatur_model.dart b/lib/app/data/models/donatur_model.dart index a931f0e..51ab672 100644 --- a/lib/app/data/models/donatur_model.dart +++ b/lib/app/data/models/donatur_model.dart @@ -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(), diff --git a/lib/app/data/models/stok_bantuan_model.dart b/lib/app/data/models/stok_bantuan_model.dart index 06721bf..46c3a01 100644 --- a/lib/app/data/models/stok_bantuan_model.dart +++ b/lib/app/data/models/stok_bantuan_model.dart @@ -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; } } diff --git a/lib/app/modules/petugas_desa/controllers/counter_service.dart b/lib/app/modules/petugas_desa/controllers/counter_service.dart index b3759a0..c379da8 100644 --- a/lib/app/modules/petugas_desa/controllers/counter_service.dart +++ b/lib/app/modules/petugas_desa/controllers/counter_service.dart @@ -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(); + // 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); } } diff --git a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart index e36ad3d..015d43a 100644 --- a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart @@ -50,6 +50,9 @@ class PenitipanBantuanController extends GetxController { // Controller untuk pencarian final TextEditingController searchController = TextEditingController(); + // Tambahkan properti untuk waktu terakhir update + Rx 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 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 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 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'); + } } diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart index bc7b75a..4999ac7 100644 --- a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart @@ -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(); @@ -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(); + 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.onTabReactivated(); + print('Memanggil onTabReactivated pada StokBantuanController'); + } catch (e) { + print('Error saat memanggil onTabReactivated: $e'); + } } } diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart index ffd698c..bba0f74 100644 --- a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -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 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 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 totalStokMap = {}; - - // Hitung total stok dari penitipan terverifikasi - for (var penitipan in daftarPenitipanTerverifikasi) { - String? stokBantuanId = penitipan['stok_bantuan_id']; - double jumlah = penitipan['jumlah'] != null - ? (penitipan['jumlah'] is int - ? penitipan['jumlah'].toDouble() - : penitipan['jumlah']) - : 0.0; - - if (stokBantuanId != null) { - if (totalStokMap.containsKey(stokBantuanId)) { - totalStokMap[stokBantuanId] = - (totalStokMap[stokBantuanId] ?? 0) + jumlah; - } else { - totalStokMap[stokBantuanId] = jumlah; - } - } - } - - // Update total stok di daftarStokBantuan - for (var i = 0; i < daftarStokBantuan.length; i++) { - var stok = daftarStokBantuan[i]; - if (stok.id != null) { - // Buat stok baru dengan total stok yang diperbarui - double newTotalStok = totalStokMap[stok.id] ?? 0.0; - - daftarStokBantuan[i] = StokBantuanModel( - id: stok.id, - nama: stok.nama, - kategoriBantuanId: stok.kategoriBantuanId, - kategoriBantuan: stok.kategoriBantuan, - totalStok: - newTotalStok, // Gunakan nilai dari penitipan atau 0 jika tidak ada - satuan: stok.satuan, - deskripsi: stok.deskripsi, - createdAt: stok.createdAt, - updatedAt: stok.updatedAt, - isUang: stok.isUang, - ); - } - } - - // Hitung ulang total dana bantuan - _hitungTotalDanaBantuan(); - } - Future loadKategoriBantuanData() async { try { final kategoriBantuanData = await _supabaseService.getKategoriBantuan(); @@ -151,17 +112,14 @@ class StokBantuanController extends GetxController { Future 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 _hitungTotalStok() async { - // Implementasi metode _hitungTotalStok - } - - Future _filterStokBantuan() async { - // Implementasi metode _filterStokBantuan - } - // Metode untuk mengatur filter void setFilter(String value) { filterValue.value = value; diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index d89ee1f..72366c4 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -31,6 +31,9 @@ class PenitipanView extends GetView { // Filter dan pencarian _buildFilterSearch(context), + // Informasi terakhir update + _buildLastUpdateInfo(context), + const SizedBox(height: 20), // Daftar penitipan @@ -43,7 +46,7 @@ class PenitipanView extends GetView { 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 { ], ), 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 { '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 { 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 { 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 { 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 { 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 { BuildContext context, Function(String) onDonaturAdded) { final formKey = GlobalKey(); 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 { ), 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( + 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( + value: 'Perorangan', + child: Text('Perorangan'), + ), + DropdownMenuItem( + value: 'Perusahaan', + child: Text('Perusahaan'), + ), + DropdownMenuItem( + value: 'Lembaga', + child: Text('Lembaga'), + ), + DropdownMenuItem( + value: 'Komunitas', + child: Text('Komunitas'), + ), + DropdownMenuItem( + 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 { 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 { ), ); } + + // 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, + ), + ), + ], + ), + ); + }); + } } diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 7aafe93..2cfe998 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -44,6 +44,9 @@ class StokBantuanView extends GetView { // 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 { ), 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 { 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( - decoration: const InputDecoration( - labelText: 'Kategori Bantuan', - border: OutlineInputBorder(), - ), - value: selectedJenisBantuanId, - hint: const Text('Pilih Kategori Bantuan'), - items: controller.daftarKategoriBantuan - .map((kategori) => DropdownMenuItem( - 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( + 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( + 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 { 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( - decoration: const InputDecoration( - labelText: 'Kategori Bantuan', - border: OutlineInputBorder(), - ), - value: selectedJenisBantuanId, - hint: const Text('Pilih Kategori Bantuan'), - isExpanded: true, - items: controller.daftarKategoriBantuan - .map((kategori) => DropdownMenuItem( - 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( + 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( + 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, + ), + ), + ], + ), + ); + }); + } } diff --git a/lib/app/utils/date_formatter.dart b/lib/app/utils/date_formatter.dart index 7ff7c00..743232f 100644 --- a/lib/app/utils/date_formatter.dart +++ b/lib/app/utils/date_formatter.dart @@ -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); + } } diff --git a/lib/main.dart b/lib/main.dart index a391546..0806f0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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); diff --git a/pubspec.lock b/pubspec.lock index 481d68c..1f005cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 6610285..115340d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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