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

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