Perbarui beberapa file konfigurasi fingerprint untuk arsitektur arm64-v8a, armeabi-v7a, x86, dan x86_64. Modifikasi tampilan dan controller di modul donatur dan petugas desa untuk meningkatkan pengalaman pengguna, termasuk penggantian logika pengambilan data dan penyesuaian tampilan. Hapus kode yang tidak digunakan dan tambahkan fungsionalitas baru untuk mendukung pengelolaan data yang lebih baik.

This commit is contained in:
Khafidh Fuadi
2025-03-27 16:55:56 +07:00
parent f74c058c71
commit f6d3eef2cf
31 changed files with 3372 additions and 1339 deletions

View File

@ -0,0 +1,306 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
class FormPengaduanView extends StatefulWidget {
final String uidPenerimaan;
final String? judul;
final String? deskripsi;
final List<File>? selectedImages;
const FormPengaduanView({
Key? key,
required this.uidPenerimaan,
this.judul,
this.deskripsi,
this.selectedImages,
}) : super(key: key);
@override
State<FormPengaduanView> createState() => _FormPengaduanViewState();
}
class _FormPengaduanViewState extends State<FormPengaduanView> {
final WargaDashboardController wargaController =
Get.find<WargaDashboardController>();
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _titleController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
bool _isSubmitting = false;
List<File> _selectedImages = [];
@override
void initState() {
super.initState();
wargaController.setAutoRefreshEnabled(false);
// Isi form dengan data yang diberikan jika ada
if (widget.judul != null) {
_titleController.text = widget.judul!;
}
if (widget.deskripsi != null) {
_descriptionController.text = widget.deskripsi!;
}
if (widget.selectedImages != null && widget.selectedImages!.isNotEmpty) {
_selectedImages = List.from(widget.selectedImages!);
}
}
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
wargaController.setAutoRefreshEnabled(true);
super.dispose();
}
Future<void> _pickImage(ImageSource source) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: source,
imageQuality: 80,
);
if (image != null) {
setState(() {
_selectedImages.add(File(image.path));
});
}
} catch (e) {
Get.snackbar(
'Error',
'Tidak dapat memilih gambar: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void _removeImage(int index) {
setState(() {
_selectedImages.removeAt(index);
});
}
Future<void> _submitComplaint() async {
if (_formKey.currentState!.validate() && !_isSubmitting) {
try {
setState(() {
_isSubmitting = true;
});
// Menutup keyboard sebelum memproses
FocusScope.of(context).unfocus();
// Menyiapkan data foto
List<String> fotoPaths =
_selectedImages.map((file) => file.path).toList();
final success = await wargaController.addPengaduan(
judul: _titleController.text,
deskripsi: _descriptionController.text,
penerimaPenyaluranId: widget.uidPenerimaan,
fotoPengaduanPaths: fotoPaths,
);
if (success) {
Get.back(result: true);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengirim pengaduan: $e',
snackPosition: SnackPosition.BOTTOM,
);
} finally {
if (mounted) {
setState(() {
_isSubmitting = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Pengaduan'),
backgroundColor: Get.theme.primaryColor,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Input Judul Pengaduan
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Judul Pengaduan',
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 16),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Judul pengaduan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
// Input Deskripsi Pengaduan
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Deskripsi Pengaduan',
border: OutlineInputBorder(),
alignLabelWithHint: true,
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 16),
),
maxLines: 5,
textInputAction: TextInputAction.done,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Deskripsi pengaduan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 20),
// Button untuk memilih gambar
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: const Text('Galeri'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: const Text('Kamera'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
// Tampilkan gambar-gambar jika sudah dipilih
if (_selectedImages.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Foto Pengaduan:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _selectedImages.length,
itemBuilder: (context, index) {
return Stack(
children: [
Container(
width: 120,
height: 120,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: FileImage(_selectedImages[index]),
fit: BoxFit.cover,
),
),
),
Positioned(
top: 5,
right: 13,
child: GestureDetector(
onTap: () => _removeImage(index),
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
);
},
),
),
],
const SizedBox(height: 24),
// Button untuk submit pengaduan
SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitComplaint,
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.orange.shade700,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Kirim Pengaduan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -5,10 +5,11 @@ import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_con
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/widgets/bantuan_card.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart';
class WargaDashboardView extends GetView<WargaDashboardController> {
const WargaDashboardView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
@ -16,7 +17,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
@ -150,10 +150,10 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: Column(
children: [
_buildInfoRow(
icon: Icons.home_rounded,
iconColor: Colors.blue.shade300,
label: 'Alamat',
value: controller.alamat ?? 'Alamat tidak tersedia',
icon: Icons.numbers_rounded,
iconColor: Colors.green.shade300,
label: 'NIK',
value: controller.nik ?? 'NIK tidak tersedia',
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
@ -175,31 +175,19 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
label: 'Desa',
value: controller.desa ?? 'Desa tidak tersedia',
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(height: 1),
),
_buildInfoRow(
icon: Icons.home_rounded,
iconColor: Colors.blue.shade300,
label: 'Alamat Lengkap',
value: controller.alamat ?? 'Alamat tidak tersedia',
),
],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildActionButton(
icon: Icons.edit_rounded,
label: 'Edit Profil',
color: Colors.blue.shade700,
onTap: () => Get.toNamed(Routes.PROFILE),
),
),
const SizedBox(width: 10),
Expanded(
child: _buildActionButton(
icon: Icons.notifications_rounded,
label: 'Notifikasi',
color: Colors.amber.shade700,
onTap: () => Get.toNamed(Routes.NOTIFIKASI),
),
),
],
),
],
),
),
@ -299,7 +287,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
}
Widget _buildStatisticSection() {
// Data untuk statistik
final totalBantuan = controller.penerimaPenyaluran.length;
final totalDiterima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'DITERIMA')
@ -341,7 +328,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
),
],
),
// Progress bar untuk persentase bantuan yang diterima
if (totalBantuan > 0) ...[
const SizedBox(height: 16),
Text(
@ -443,7 +429,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
decimalDigits: 0,
);
// Hitung total bantuan uang dan non-uang
double totalUang = 0;
Map<String, double> totalNonUang = {};
@ -742,4 +727,146 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
),
);
}
void _showBuatPengaduanDialog(BuildContext context) {
// Daftar penerimaan bantuan yang dapat diadukan (status DITERIMA)
final bantuanDiterima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'DITERIMA')
.toList();
// Jika tidak ada bantuan yang diterima
if (bantuanDiterima.isEmpty) {
Get.snackbar(
'Informasi',
'Tidak ada bantuan yang sudah diterima untuk dapat diajukan pengaduan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
return;
}
// Variabel untuk menyimpan pilihan penerimaan
PenerimaPenyaluranModel? selectedPenerimaan = bantuanDiterima.first;
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.report_problem,
color: Colors.orange.shade700,
),
const SizedBox(width: 10),
const Text(
'Buat Pengaduan Baru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 20),
const Text(
'Pilih Bantuan yang Ingin Diadukan:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 10),
StatefulBuilder(
builder: (context, setState) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<PenerimaPenyaluranModel>(
isExpanded: true,
value: selectedPenerimaan,
items: bantuanDiterima.map((item) {
String displayText = item.namaPenyaluran ?? 'Bantuan';
if (item.tanggalPenerimaan != null) {
displayText +=
' (${DateFormat('dd/MM/yyyy').format(item.tanggalPenerimaan!)})';
}
return DropdownMenuItem<PenerimaPenyaluranModel>(
value: item,
child: Text(displayText),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
selectedPenerimaan = value;
});
}
},
),
),
);
},
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (selectedPenerimaan != null) {
Get.back();
Get.to(
() => FormPengaduanView(
uidPenerimaan: selectedPenerimaan!.id.toString(),
),
transition: Transition.rightToLeft,
duration: const Duration(milliseconds: 300),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade700,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Lanjutkan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
);
}
}

View File

@ -1301,13 +1301,13 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
Get.back(); // Tutup dialog terlebih dahulu
// Tampilkan loading
Get.dialog(
const Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
// // Tampilkan loading
// Get.dialog(
// const Center(
// child: CircularProgressIndicator(),
// ),
// barrierDismissible: false,
// );
bool success = false;
try {
@ -1336,18 +1336,6 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
// Refresh data halaman
await controller.fetchPengaduan();
await controller.fetchPenerimaPenyaluran();
Get.snackbar(
'Sukses',
'Pengaduan berhasil dikirim dan data diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Navigate ke halaman pengaduan jika berhasil
controller.changeTab(2); // Tab pengaduan
}
},
style: ElevatedButton.styleFrom(

View File

@ -1,40 +1,48 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
class WargaPengaduanView extends GetView<WargaDashboardController> {
const WargaPengaduanView({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// Debug print untuk melihat jumlah item
print('DEBUG: Jumlah pengaduan tersedia: ${controller.pengaduan.length}');
// Debug print untuk melihat jumlah item
print(
'DEBUG: Jumlah pengaduan tersedia: ${controller.pengaduan.length}');
return RefreshIndicator(
onRefresh: () async {
// Tambahkan delay untuk memastikan refresh indicator terlihat
await Future.delayed(const Duration(milliseconds: 300));
controller.fetchData();
},
child: controller.pengaduan.isEmpty
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: Get.height * 0.7,
child: _buildEmptyState(),
),
],
)
: _buildPengaduanList(context),
);
});
return RefreshIndicator(
onRefresh: () async {
// Tambahkan delay untuk memastikan refresh indicator terlihat
await Future.delayed(const Duration(milliseconds: 300));
controller.fetchData();
},
child: controller.pengaduan.isEmpty
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: Get.height * 0.7,
child: _buildEmptyState(),
),
],
)
: _buildPengaduanList(context),
);
}),
);
}
Widget _buildEmptyState() {
@ -89,8 +97,6 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
}
final item = controller.pengaduan[index];
print(
'DEBUG: Membangun item pengaduan $index dengan id: ${item.id}');
// Tentukan status dan warna berdasarkan status pengaduan
Color statusColor;

View File

@ -16,16 +16,16 @@ class WargaView extends GetView<WargaDashboardController> {
Widget build(BuildContext context) {
// Tambahkan listener untuk refresh data saat fokus didapatkan kembali
// misalnya ketika kembali dari halaman profil
WidgetsBinding.instance.addPostFrameCallback((_) {
final focusNode = FocusNode();
FocusScope.of(context).requestFocus(focusNode);
focusNode.addListener(() {
if (focusNode.hasFocus) {
print('DEBUG WARGA: Halaman mendapatkan fokus, memuat ulang data');
controller.refreshData();
}
});
});
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final focusNode = FocusNode();
// FocusScope.of(context).requestFocus(focusNode);
// focusNode.addListener(() {
// if (focusNode.hasFocus) {
// print('DEBUG WARGA: Halaman mendapatkan fokus, memuat ulang data');
// controller.refreshData();
// }
// });
// });
return Scaffold(
key: scaffoldKey,