Tambahkan fungsionalitas pendaftaran donatur baru tanpa konfirmasi email di AuthProvider. Perbarui model DonaturModel untuk menyertakan properti isManual. Modifikasi tampilan dan controller untuk mendukung registrasi donatur, termasuk validasi form dan navigasi ke halaman pendaftaran. Perbarui rute aplikasi untuk menambahkan halaman pendaftaran donatur. Selain itu, perbarui beberapa file konfigurasi dan dependensi untuk mendukung perubahan ini.
This commit is contained in:
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
@ -44,11 +43,6 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showTambahPenitipanDialog(context),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -293,6 +287,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
// Gunakan data donatur dari relasi jika tersedia
|
||||
final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan';
|
||||
|
||||
// Cek apakah donatur manual
|
||||
final isDonaturManual = item.donatur?.isManual ?? false;
|
||||
|
||||
// Debug info
|
||||
print('PenitipanItem - stokBantuanId: ${item.stokBantuanId}');
|
||||
|
||||
@ -331,12 +328,43 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
donaturNama,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
donaturNama,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (isDonaturManual)
|
||||
Tooltip(
|
||||
message: 'Donatur Manual (Diinput oleh petugas desa)',
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.blue.shade300),
|
||||
),
|
||||
child: const Text(
|
||||
'Manual',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@ -753,771 +781,6 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showTambahPenitipanDialog(BuildContext context) {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final TextEditingController jumlahController = TextEditingController();
|
||||
final TextEditingController deskripsiController = TextEditingController();
|
||||
|
||||
// Variabel untuk menyimpan nilai yang dipilih
|
||||
final Rx<String?> selectedStokBantuanId = Rx<String?>(null);
|
||||
final Rx<String?> selectedDonaturId = Rx<String?>(null);
|
||||
final Rx<DonaturModel?> selectedDonatur = Rx<DonaturModel?>(null);
|
||||
|
||||
// Reset foto bantuan paths
|
||||
controller.fotoBantuanPaths.clear();
|
||||
controller.donaturSearchController.clear();
|
||||
controller.hasilPencarianDonatur.clear();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
// Dapatkan informasi apakah stok bantuan berupa uang
|
||||
bool isUang = false;
|
||||
String satuan = '';
|
||||
if (selectedStokBantuanId.value != null) {
|
||||
isUang =
|
||||
controller.isStokBantuanUang(selectedStokBantuanId.value!);
|
||||
satuan =
|
||||
controller.getKategoriSatuan(selectedStokBantuanId.value);
|
||||
}
|
||||
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Manual Penitipan Bantuan',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Pilih kategori bantuan
|
||||
Text(
|
||||
'Jenis Stok Bantuan',
|
||||
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 stok bantuan'),
|
||||
value: selectedStokBantuanId.value,
|
||||
items: controller.stokBantuanMap.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(entry.value.nama ?? 'Tidak ada nama'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
selectedStokBantuanId.value = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kategori bantuan harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Jumlah bantuan
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isUang ? 'Jumlah Uang (Rp)' : 'Jumlah Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: jumlahController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText:
|
||||
isUang ? 'Contoh: 100000' : 'Contoh: 10',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Jumlah harus diisi';
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Jumlah harus berupa angka';
|
||||
}
|
||||
if (double.parse(value) <= 0) {
|
||||
return 'Jumlah harus lebih dari 0';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (satuan.isNotEmpty && !isUang) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 32),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
),
|
||||
child: Text(
|
||||
satuan,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Donatur (wajib)
|
||||
Text(
|
||||
'Donatur',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (selectedDonatur.value != null) ...[
|
||||
// Tampilkan donatur yang dipilih
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
selectedDonatur.value!.nama ??
|
||||
'Tidak ada nama',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (selectedDonatur.value!.noHp != null)
|
||||
Text(selectedDonatur.value!.noHp!),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
selectedDonatur.value = null;
|
||||
selectedDonaturId.value = null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Tampilkan pencarian donatur
|
||||
TextFormField(
|
||||
controller: controller.donaturSearchController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Cari donatur (min. 3 karakter)',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
suffixIcon: controller.isSearchingDonatur.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.search),
|
||||
),
|
||||
onChanged: (value) {
|
||||
controller.searchDonatur(value);
|
||||
},
|
||||
validator: (value) {
|
||||
if (selectedDonaturId.value == null) {
|
||||
return 'Donatur harus dipilih';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
// Hasil pencarian donatur
|
||||
if (controller.hasilPencarianDonatur.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 150),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount:
|
||||
controller.hasilPencarianDonatur.length,
|
||||
itemBuilder: (context, index) {
|
||||
final donatur =
|
||||
controller.hasilPencarianDonatur[index];
|
||||
return ListTile(
|
||||
title:
|
||||
Text(donatur.nama ?? 'Tidak ada nama'),
|
||||
subtitle: donatur.noHp != null
|
||||
? Text(donatur.noHp!)
|
||||
: const Text('Tidak ada nomor telepon'),
|
||||
dense: true,
|
||||
onTap: () {
|
||||
selectedDonatur.value = donatur;
|
||||
selectedDonaturId.value = donatur.id;
|
||||
controller.donaturSearchController
|
||||
.clear();
|
||||
controller.hasilPencarianDonatur.clear();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Tombol tambah donatur baru
|
||||
if (controller.donaturSearchController.text.length >=
|
||||
3 &&
|
||||
controller.hasilPencarianDonatur.isEmpty &&
|
||||
!controller.isSearchingDonatur.value)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_showTambahDonaturDialog(context,
|
||||
(donaturId) {
|
||||
// Callback ketika donatur berhasil ditambahkan
|
||||
controller
|
||||
.getDonaturInfo(donaturId)
|
||||
.then((donatur) {
|
||||
if (donatur != null) {
|
||||
selectedDonatur.value = donatur;
|
||||
selectedDonaturId.value = donatur.id;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Deskripsi
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: deskripsiController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Deskripsi bantuan',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Deskripsi harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Upload foto bantuan
|
||||
Text(
|
||||
'Foto Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (controller.fotoBantuanPaths.isEmpty)
|
||||
InkWell(
|
||||
onTap: () => _showPilihSumberFoto(context),
|
||||
child: Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt,
|
||||
size: 48,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: controller.fotoBantuanPaths.length +
|
||||
1, // +1 untuk tombol tambah
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
controller.fotoBantuanPaths.length) {
|
||||
// Tombol tambah foto
|
||||
return InkWell(
|
||||
onTap: () => _showPilihSumberFoto(context),
|
||||
child: Container(
|
||||
width: 100,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade400),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate,
|
||||
size: 32,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan foto yang sudah diambil
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: FileImage(File(controller
|
||||
.fotoBantuanPaths[index])),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 12,
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
controller.removeFotoBantuan(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Colors.black.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: controller.isUploading.value
|
||||
? null
|
||||
: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
if (controller.fotoBantuanPaths.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Foto bantuan harus diupload',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.tambahPenitipanBantuan(
|
||||
stokBantuanId:
|
||||
selectedStokBantuanId.value!,
|
||||
jumlah:
|
||||
double.parse(jumlahController.text),
|
||||
deskripsi: deskripsiController.text,
|
||||
donaturId: selectedDonaturId.value,
|
||||
isUang: isUang,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: controller.isUploading.value
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPilihSumberFoto(BuildContext context) {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Pilih Sumber Foto',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Kamera'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickFotoBantuan(fromCamera: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Galeri'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickFotoBantuan(fromCamera: false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTambahDonaturDialog(
|
||||
BuildContext context, Function(String) onDonaturAdded) {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final TextEditingController namaController = TextEditingController();
|
||||
final TextEditingController noHpController = TextEditingController();
|
||||
final TextEditingController alamatController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController jenisController = TextEditingController();
|
||||
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tambah Donatur Baru',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nama donatur
|
||||
Text(
|
||||
'Nama Donatur',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: namaController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan nama donatur',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama donatur harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Telepon
|
||||
Text(
|
||||
'Nomor HP',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: noHpController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan nomor HP',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nomor HP 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: 'Individu',
|
||||
child: Text('Individu'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Perusahaan',
|
||||
child: Text('Perusahaan'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'Organisasi',
|
||||
child: Text('Organisasi'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
jenisController.text = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Alamat (opsional)
|
||||
Text(
|
||||
'Alamat (Opsional)',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: alamatController,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan alamat',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email (opsional)
|
||||
Text(
|
||||
'Email (Opsional)',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
hintText: 'Masukkan email',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final donaturId = await controller.tambahDonatur(
|
||||
nama: namaController.text,
|
||||
noHp: noHpController.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) {
|
||||
Get.back();
|
||||
onDonaturAdded(donaturId);
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Donatur berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: const Text('Simpan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
return Obx(() {
|
||||
|
@ -5,9 +5,11 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dar
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/stok_bantuan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_stok_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
|
||||
|
||||
class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
const PetugasDesaView({super.key});
|
||||
@ -141,6 +143,27 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan tombol riwayat stok jika tab Stok Bantuan aktif
|
||||
if (activeTab == 4) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman riwayat stok
|
||||
if (!Get.isRegistered<RiwayatStokController>()) {
|
||||
Get.put(RiwayatStokController());
|
||||
}
|
||||
Get.to(() => const RiwayatStokView());
|
||||
},
|
||||
icon: const Icon(Icons.history),
|
||||
tooltip: 'Riwayat Stok',
|
||||
),
|
||||
notificationButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return notificationButton;
|
||||
}),
|
||||
],
|
||||
|
688
lib/app/modules/petugas_desa/views/riwayat_stok_view.dart
Normal file
688
lib/app/modules/petugas_desa/views/riwayat_stok_view.dart
Normal file
@ -0,0 +1,688 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/riwayat_stok_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
const RiwayatStokView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Riwayat Stok Bantuan'),
|
||||
//back button
|
||||
leading: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: controller.refreshData,
|
||||
child: Obx(() => controller.isLoading.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildContent(context)),
|
||||
),
|
||||
floatingActionButton: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tombol untuk mengurangi stok
|
||||
FloatingActionButton.small(
|
||||
onPressed: () => _showStokManualDialog(context, isAddition: false),
|
||||
backgroundColor: Colors.red,
|
||||
heroTag: 'kurangiStok',
|
||||
child: const Icon(Icons.remove, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Tombol untuk menambah stok
|
||||
FloatingActionButton(
|
||||
onPressed: () => _showStokManualDialog(context, isAddition: true),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
heroTag: 'tambahStok',
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Heading
|
||||
Text(
|
||||
'Riwayat Stok Bantuan',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Catatan riwayat perubahan stok bantuan',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filter dan pencarian
|
||||
_buildFilters(context),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Daftar riwayat stok
|
||||
Obx(() {
|
||||
final filteredRiwayat = controller.getFilteredRiwayatStok();
|
||||
if (filteredRiwayat.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.history,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada riwayat stok',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredRiwayat.length,
|
||||
itemBuilder: (context, index) {
|
||||
final riwayat = filteredRiwayat[index];
|
||||
return _buildRiwayatItem(context, riwayat);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Pencarian
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Cari riwayat stok...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filter jenis perubahan
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Jenis Perubahan'),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Obx(() => DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
isExpanded: true,
|
||||
value: controller.filterJenisPerubahan.value,
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: 'semua',
|
||||
child: Text('Semua'),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: 'penambahan',
|
||||
child: Text('Penambahan'),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: 'pengurangan',
|
||||
child: Text('Pengurangan'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.filterByJenisPerubahan(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Jenis Bantuan'),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Obx(() => DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
isExpanded: true,
|
||||
value: controller.filterStokBantuanId.value,
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: 'semua',
|
||||
child: Text('Semua'),
|
||||
),
|
||||
...controller.daftarStokBantuan.map((stok) {
|
||||
return DropdownMenuItem(
|
||||
value: stok.id,
|
||||
child: Text(stok.nama ?? '-'),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.filterByStokBantuan(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRiwayatItem(BuildContext context, RiwayatStokModel riwayat) {
|
||||
final bool isPenambahan = riwayat.jenisPerubahan == 'penambahan';
|
||||
final stokBantuanNama = riwayat.stokBantuan != null
|
||||
? riwayat.stokBantuan!['nama'] ?? 'Tidak diketahui'
|
||||
: 'Tidak diketahui';
|
||||
final stokBantuanSatuan =
|
||||
riwayat.stokBantuan != null ? riwayat.stokBantuan!['satuan'] ?? '' : '';
|
||||
final sumberLabels = {
|
||||
'penitipan': 'Penitipan',
|
||||
'penyaluran': 'Penyaluran',
|
||||
'manual': 'Manual',
|
||||
};
|
||||
final sumberLabel = sumberLabels[riwayat.sumber] ?? 'Tidak diketahui';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: Jumlah dan waktu
|
||||
Row(
|
||||
children: [
|
||||
// Icon & Jumlah
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isPenambahan ? Colors.green[100] : Colors.red[100],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isPenambahan ? Icons.add : Icons.remove,
|
||||
color: isPenambahan ? Colors.green : Colors.red,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isPenambahan ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Sumber
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
sumberLabel,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Tanggal
|
||||
Text(
|
||||
riwayat.createdAt != null
|
||||
? DateTimeHelper.formatDateTime(riwayat.createdAt!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Nama bantuan
|
||||
Text(
|
||||
stokBantuanNama,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
// Alasan jika ada
|
||||
if (riwayat.alasan != null && riwayat.alasan!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Alasan: ${riwayat.alasan}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
|
||||
// Foto bukti jika ada
|
||||
if (riwayat.fotoBukti != null && riwayat.fotoBukti!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => _showImageDialog(context, riwayat.fotoBukti!),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.photo,
|
||||
color: Colors.blue,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Lihat Bukti',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Petugas
|
||||
if (riwayat.createdBy != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Oleh: ${riwayat.createdBy!['nama_lengkap'] ?? 'Tidak diketahui'}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showImageDialog(BuildContext context, String imageUrl) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: const Text('Bukti Foto'),
|
||||
elevation: 0,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
InteractiveViewer(
|
||||
panEnabled: true,
|
||||
boundaryMargin: const EdgeInsets.all(16),
|
||||
minScale: 0.5,
|
||||
maxScale: 4,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error),
|
||||
const SizedBox(height: 8),
|
||||
Text('Gagal memuat gambar: $error'),
|
||||
],
|
||||
),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
|
||||
// Reset form
|
||||
controller.resetForm();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isAddition ? Icons.add_circle : Icons.remove_circle,
|
||||
color: isAddition ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isAddition
|
||||
? 'Tambah Stok Manual'
|
||||
: 'Kurangi Stok Manual',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Form
|
||||
// 1. Pilih Bantuan
|
||||
Text(
|
||||
'Pilih Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(() => DropdownButtonFormField<StokBantuanModel>(
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 12),
|
||||
),
|
||||
hint: const Text('Pilih bantuan'),
|
||||
value: controller.selectedStokBantuan.value,
|
||||
items: controller.daftarStokBantuan
|
||||
.map((StokBantuanModel stok) {
|
||||
return DropdownMenuItem<StokBantuanModel>(
|
||||
value: stok,
|
||||
child: Text(
|
||||
'${stok.nama} (${stok.totalStok} ${stok.satuan})'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (StokBantuanModel? value) {
|
||||
controller.setSelectedStokBantuan(value);
|
||||
},
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. Jumlah
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Jumlah',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Tampilkan satuan jika bantuan sudah dipilih
|
||||
Obx(() => controller.selectedStokBantuan.value != null
|
||||
? Text(
|
||||
controller.selectedStokBantuan.value!.satuan ??
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 12),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
controller.setJumlah(double.parse(value));
|
||||
} else {
|
||||
controller.setJumlah(0);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 3. Alasan
|
||||
Text(
|
||||
'Alasan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 12),
|
||||
hintText: 'Masukkan alasan perubahan stok',
|
||||
),
|
||||
maxLines: 2,
|
||||
onChanged: (value) {
|
||||
controller.setAlasan(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. Upload Bukti
|
||||
Text(
|
||||
'Foto Bukti',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: controller.pickImage,
|
||||
child: Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[400]!),
|
||||
),
|
||||
child: Obx(() {
|
||||
if (controller.fotoBukti.value != null) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.file(
|
||||
controller.fotoBukti.value!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.red,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.fotoBukti.value = null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.camera_alt,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pilih Foto',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => ElevatedButton(
|
||||
onPressed: controller.isSubmitting.value
|
||||
? null
|
||||
: () {
|
||||
if (isAddition) {
|
||||
controller.tambahStokManual();
|
||||
} else {
|
||||
controller.kurangiStokManual();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isAddition ? Colors.green : Colors.red,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: controller.isSubmitting.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
isAddition ? 'Tambah Stok' : 'Kurangi Stok',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user