Hapus pernyataan print yang tidak diperlukan di DetailPenyaluranController untuk meningkatkan kebersihan kode. Perbarui filter kategori di PengaduanController untuk menyertakan opsi baru "Semua Kecuali Selesai". Modifikasi tampilan di PengaduanView untuk menampilkan ringkasan pengaduan dengan lebih baik dan tambahkan rute baru untuk RiwayatPengaduan.

This commit is contained in:
Khafidh Fuadi
2025-03-17 21:18:32 +07:00
parent 9eb2c5ac1a
commit 7ee56903ee
10 changed files with 676 additions and 160 deletions

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_pengaduan_controller.dart';
class RiwayatPengaduanBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RiwayatPengaduanController>(
() => RiwayatPengaduanController(),
);
}
}

View File

@ -38,7 +38,6 @@ class DetailPenyaluranController extends GetxController {
checkUserRole(); checkUserRole();
} else { } else {
isLoading.value = false; isLoading.value = false;
print('DetailPenyaluranController - ID Penyaluran tidak ditemukan');
} }
} }
@ -64,8 +63,6 @@ class DetailPenyaluranController extends GetxController {
Future<void> loadPenyaluranData(String penyaluranId) async { Future<void> loadPenyaluranData(String penyaluranId) async {
try { try {
isLoading.value = true; isLoading.value = true;
print(
'DetailPenyaluranController - Memuat data penyaluran dengan ID: $penyaluranId');
// Ambil data penyaluran // Ambil data penyaluran
final penyaluranData = await _supabaseService.client final penyaluranData = await _supabaseService.client
@ -74,8 +71,6 @@ class DetailPenyaluranController extends GetxController {
.eq('id', penyaluranId) .eq('id', penyaluranId)
.single(); .single();
print('DetailPenyaluranController - Data penyaluran: $penyaluranData');
// Pastikan data yang diterima sesuai dengan tipe data yang diharapkan // Pastikan data yang diterima sesuai dengan tipe data yang diharapkan
Map<String, dynamic> sanitizedData = Map<String, dynamic> sanitizedData =
Map<String, dynamic>.from(penyaluranData); Map<String, dynamic>.from(penyaluranData);
@ -87,42 +82,33 @@ class DetailPenyaluranController extends GetxController {
} }
penyaluran.value = PenyaluranBantuanModel.fromJson(sanitizedData); penyaluran.value = PenyaluranBantuanModel.fromJson(sanitizedData);
print(
'DetailPenyaluranController - Model penyaluran: ${penyaluran.value?.nama}');
// Ambil data skema bantuan jika ada // Ambil data skema bantuan jika ada
if (penyaluran.value?.skemaId != null && if (penyaluran.value?.skemaId != null &&
penyaluran.value!.skemaId!.isNotEmpty) { penyaluran.value!.skemaId!.isNotEmpty) {
print(
'DetailPenyaluranController - Memuat skema bantuan dengan ID: ${penyaluran.value!.skemaId}');
final skemaData = await _supabaseService.client final skemaData = await _supabaseService.client
.from('xx02_skema_bantuan') .from('xx02_skema_bantuan')
.select('*') .select('*')
.eq('id', penyaluran.value!.skemaId!) .eq('id', penyaluran.value!.skemaId!)
.single(); .single();
print('DetailPenyaluranController - Data skema bantuan: $skemaData'); // Pastikan data skema sesuai dengan tipe data yang diharapkan
if (skemaData != null) { Map<String, dynamic> sanitizedSkemaData =
// Pastikan data skema sesuai dengan tipe data yang diharapkan Map<String, dynamic>.from(skemaData);
Map<String, dynamic> sanitizedSkemaData =
Map<String, dynamic>.from(skemaData);
// Konversi kuota ke int jika bertipe String // Konversi kuota ke int jika bertipe String
if (sanitizedSkemaData['kuota'] is String) { if (sanitizedSkemaData['kuota'] is String) {
sanitizedSkemaData['kuota'] = sanitizedSkemaData['kuota'] =
int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0; int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0;
}
// Konversi petugas_verifikasi_id ke int jika bertipe String
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
sanitizedSkemaData['petugas_verifikasi_id'] as String);
}
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
print(
'DetailPenyaluranController - Model skema bantuan: ${skemaBantuan.value?.nama}');
} }
// Konversi petugas_verifikasi_id ke int jika bertipe String
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
sanitizedSkemaData['petugas_verifikasi_id'] as String);
}
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
} }
// Ambil data penerima penyaluran // Ambil data penerima penyaluran
@ -131,32 +117,22 @@ class DetailPenyaluranController extends GetxController {
.select('*, warga:warga_id(*)') .select('*, warga:warga_id(*)')
.eq('penyaluran_bantuan_id', penyaluranId); .eq('penyaluran_bantuan_id', penyaluranId);
print( final List<PenerimaPenyaluranModel> penerima = [];
'DetailPenyaluranController - Data penerima penyaluran: $penerimaPenyaluranData'); for (var item in penerimaPenyaluranData) {
if (penerimaPenyaluranData != null) { // Pastikan data penerima sesuai dengan tipe data yang diharapkan
final List<PenerimaPenyaluranModel> penerima = []; Map<String, dynamic> sanitizedPenerimaData =
for (var item in penerimaPenyaluranData) { Map<String, dynamic>.from(item);
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
Map<String, dynamic> sanitizedPenerimaData =
Map<String, dynamic>.from(item);
// Konversi jumlah_bantuan ke double jika bertipe String // Konversi jumlah_bantuan ke double jika bertipe String
if (sanitizedPenerimaData['jumlah_bantuan'] is String) { if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse( sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse(
sanitizedPenerimaData['jumlah_bantuan'] as String); sanitizedPenerimaData['jumlah_bantuan'] as String);
}
penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
} }
penerimaPenyaluran.assignAll(penerima);
print(
'DetailPenyaluranController - Jumlah penerima: ${penerima.length}');
//print id penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
} }
penerimaPenyaluran.assignAll(penerima);
} catch (e) { } catch (e) {
print('Error loading penyaluran data: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data penyaluran', 'Terjadi kesalahan saat memuat data penyaluran',
@ -198,7 +174,6 @@ class DetailPenyaluranController extends GetxController {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
} catch (e) { } catch (e) {
print('Error memulai penyaluran: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memulai penyaluran bantuan', 'Terjadi kesalahan saat memulai penyaluran bantuan',
@ -237,10 +212,6 @@ class DetailPenyaluranController extends GetxController {
'tanda_tangan': tandaTangan, 'tanda_tangan': tandaTangan,
}; };
print(
'DetailPenyaluranController - Updating penerima with ID: ${penerima.id}');
print('DetailPenyaluranController - Update data: $updateData');
await _supabaseService.client await _supabaseService.client
.from('penerima_penyaluran') .from('penerima_penyaluran')
.update(updateData) .update(updateData)
@ -251,8 +222,6 @@ class DetailPenyaluranController extends GetxController {
// Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima // Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima
} catch (e) { } catch (e) {
print('Error konfirmasi penerimaan: $e');
// Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima
rethrow; // Melempar kembali exception agar dapat ditangkap di _konfirmasiPenerimaan rethrow; // Melempar kembali exception agar dapat ditangkap di _konfirmasiPenerimaan
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
@ -314,7 +283,6 @@ class DetailPenyaluranController extends GetxController {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
} catch (e) { } catch (e) {
print('Error menyelesaikan penyaluran: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat menyelesaikan penyaluran bantuan', 'Terjadi kesalahan saat menyelesaikan penyaluran bantuan',
@ -353,7 +321,6 @@ class DetailPenyaluranController extends GetxController {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
} catch (e) { } catch (e) {
print('Error membatalkan penyaluran: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat membatalkan penyaluran bantuan', 'Terjadi kesalahan saat membatalkan penyaluran bantuan',
@ -378,21 +345,14 @@ class DetailPenyaluranController extends GetxController {
'${filePrefix}_${DateTime.now().millisecondsSinceEpoch}.jpg'; '${filePrefix}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final file = File(filePath); final file = File(filePath);
print(
'Uploading ${isTandaTangan ? "tanda tangan" : "bukti penerimaan"} dari: $filePath');
print('File exists: ${file.existsSync()}');
print('File size: ${file.lengthSync()} bytes');
if (!file.existsSync()) { if (!file.existsSync()) {
throw Exception('File tidak ditemukan: $filePath'); throw Exception('File tidak ditemukan: $filePath');
} }
print('Uploading ke bucket: $folderName dengan nama file: $fileName');
final storageResponse = await _supabaseService.client.storage final storageResponse = await _supabaseService.client.storage
.from(folderName) .from(folderName)
.upload(fileName, file); .upload(fileName, file);
print('Storage response: $storageResponse');
if (storageResponse.isEmpty) { if (storageResponse.isEmpty) {
throw Exception( throw Exception(
'Gagal mengupload ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}'); 'Gagal mengupload ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}');
@ -402,7 +362,6 @@ class DetailPenyaluranController extends GetxController {
.from(folderName) .from(folderName)
.getPublicUrl(fileName); .getPublicUrl(fileName);
print('File URL: $fileUrl');
if (fileUrl.isEmpty) { if (fileUrl.isEmpty) {
throw Exception( throw Exception(
'Gagal mendapatkan URL ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}'); 'Gagal mendapatkan URL ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}');
@ -422,42 +381,33 @@ class DetailPenyaluranController extends GetxController {
Future<void> loadPenyaluranDetails(String penyaluranId) async { Future<void> loadPenyaluranDetails(String penyaluranId) async {
try { try {
isLoading.value = true; isLoading.value = true;
print(
'DetailPenyaluranController - Memuat detail penyaluran dengan ID: $penyaluranId');
// Ambil data skema bantuan jika ada // Ambil data skema bantuan jika ada
if (penyaluran.value?.skemaId != null && if (penyaluran.value?.skemaId != null &&
penyaluran.value!.skemaId!.isNotEmpty) { penyaluran.value!.skemaId!.isNotEmpty) {
print(
'DetailPenyaluranController - Memuat skema bantuan dengan ID: ${penyaluran.value!.skemaId}');
final skemaData = await _supabaseService.client final skemaData = await _supabaseService.client
.from('xx02_skema_bantuan') .from('xx02_skema_bantuan')
.select('*') .select('*')
.eq('id', penyaluran.value!.skemaId!) .eq('id', penyaluran.value!.skemaId!)
.single(); .single();
print('DetailPenyaluranController - Data skema bantuan: $skemaData'); // Pastikan data skema sesuai dengan tipe data yang diharapkan
if (skemaData != null) { Map<String, dynamic> sanitizedSkemaData =
// Pastikan data skema sesuai dengan tipe data yang diharapkan Map<String, dynamic>.from(skemaData);
Map<String, dynamic> sanitizedSkemaData =
Map<String, dynamic>.from(skemaData);
// Konversi kuota ke int jika bertipe String // Konversi kuota ke int jika bertipe String
if (sanitizedSkemaData['kuota'] is String) { if (sanitizedSkemaData['kuota'] is String) {
sanitizedSkemaData['kuota'] = sanitizedSkemaData['kuota'] =
int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0; int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0;
}
// Konversi petugas_verifikasi_id ke int jika bertipe String
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
sanitizedSkemaData['petugas_verifikasi_id'] as String);
}
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
print(
'DetailPenyaluranController - Model skema bantuan: ${skemaBantuan.value?.nama}');
} }
// Konversi petugas_verifikasi_id ke int jika bertipe String
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
sanitizedSkemaData['petugas_verifikasi_id'] as String);
}
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
} }
// Ambil data penerima penyaluran // Ambil data penerima penyaluran
@ -466,33 +416,26 @@ class DetailPenyaluranController extends GetxController {
.select('*, warga:warga_id(*)') .select('*, warga:warga_id(*)')
.eq('penyaluran_bantuan_id', penyaluranId); .eq('penyaluran_bantuan_id', penyaluranId);
print( final List<PenerimaPenyaluranModel> penerima = [];
'DetailPenyaluranController - Data penerima penyaluran: $penerimaPenyaluranData'); for (var item in penerimaPenyaluranData) {
if (penerimaPenyaluranData != null) { // Pastikan data penerima sesuai dengan tipe data yang diharapkan
final List<PenerimaPenyaluranModel> penerima = []; Map<String, dynamic> sanitizedPenerimaData =
for (var item in penerimaPenyaluranData) { Map<String, dynamic>.from(item);
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
Map<String, dynamic> sanitizedPenerimaData =
Map<String, dynamic>.from(item);
// Konversi jumlah_bantuan ke double jika bertipe String // Konversi jumlah_bantuan ke double jika bertipe String
if (sanitizedPenerimaData['jumlah_bantuan'] is String) { if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse( sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse(
sanitizedPenerimaData['jumlah_bantuan'] as String); sanitizedPenerimaData['jumlah_bantuan'] as String);
}
penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
} }
penerimaPenyaluran.assignAll(penerima);
print(
'DetailPenyaluranController - Jumlah penerima: ${penerima.length}');
if (penerima.isNotEmpty) { penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
}
} }
penerimaPenyaluran.assignAll(penerima);
// if (penerima.isNotEmpty) {
// print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
// }
} catch (e) { } catch (e) {
print('Error loading penyaluran details: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat detail penyaluran', 'Terjadi kesalahan saat memuat detail penyaluran',

View File

@ -15,7 +15,7 @@ class PengaduanController extends GetxController {
final RxBool isUploading = false.obs; final RxBool isUploading = false.obs;
// Indeks kategori yang dipilih untuk filter // Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs; final RxInt selectedCategoryIndex = 4.obs;
// Data untuk pengaduan // Data untuk pengaduan
final RxList<PengaduanModel> daftarPengaduan = <PengaduanModel>[].obs; final RxList<PengaduanModel> daftarPengaduan = <PengaduanModel>[].obs;
@ -346,6 +346,10 @@ class PengaduanController extends GetxController {
return daftarPengaduan return daftarPengaduan
.where((item) => item.status == 'SELESAI') .where((item) => item.status == 'SELESAI')
.toList(); .toList();
case 4:
return daftarPengaduan
.where((item) => item.status != 'SELESAI')
.toList();
default: default:
return daftarPengaduan; return daftarPengaduan;
} }

View File

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
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/services/supabase_service.dart';
class RiwayatPengaduanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Data untuk pengaduan
final RxList<PengaduanModel> daftarRiwayatPengaduan = <PengaduanModel>[].obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadRiwayatPengaduanData();
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
Future<void> loadRiwayatPengaduanData() async {
isLoading.value = true;
try {
final pengaduanData =
await _supabaseService.getPengaduanWithPenerimaPenyaluran();
if (pengaduanData != null) {
// Filter hanya pengaduan dengan status SELESAI
final List<PengaduanModel> selesaiPengaduan = pengaduanData
.map((data) => PengaduanModel.fromJson(data))
.where((item) => item.status == 'SELESAI')
.toList();
daftarRiwayatPengaduan.value = selesaiPengaduan;
}
} catch (e) {
print('Error loading riwayat pengaduan data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadRiwayatPengaduanData();
} finally {
isLoading.value = false;
}
}
List<PengaduanModel> getFilteredRiwayatPengaduan() {
if (searchController.text.isEmpty) {
return daftarRiwayatPengaduan;
}
final searchQuery = searchController.text.toLowerCase();
return daftarRiwayatPengaduan.where((item) {
final namaWarga = item.warga?['nama']?.toString().toLowerCase() ?? '';
final nik = item.warga?['nik']?.toString().toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
return namaWarga.contains(searchQuery) ||
nik.contains(searchQuery) ||
deskripsi.contains(searchQuery);
}).toList();
}
Future<Map<String, dynamic>> getDetailPengaduan(String pengaduanId) async {
try {
// Ambil data pengaduan
final pengaduanData =
await _supabaseService.client.from('pengaduan').select('''
*,
penerima_penyaluran:penerima_penyaluran_id(
*,
penyaluran_bantuan:penyaluran_bantuan_id(*),
stok_bantuan:stok_bantuan_id(*),
warga:warga_id(*)
),
warga:warga_id(*)
''').eq('id', pengaduanId).single();
// Ambil data tindakan pengaduan
final tindakanData =
await _supabaseService.getTindakanPengaduan(pengaduanId);
// Gabungkan data
final result = {
'pengaduan': pengaduanData,
'tindakan': tindakanData ?? [],
};
return result;
} catch (e) {
print('Error getting detail pengaduan: $e');
return {
'pengaduan': null,
'tindakan': [],
};
}
}
}

View File

@ -11,7 +11,6 @@ import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:timeline_tile/timeline_tile.dart'; import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:io'; import 'dart:io';
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart'; import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart'; import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart'; import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
class PengaduanView extends GetView<PengaduanController> { class PengaduanView extends GetView<PengaduanController> {
const PengaduanView({super.key}); const PengaduanView({super.key});
@ -67,57 +68,61 @@ class PengaduanView extends GetView<PengaduanController> {
Widget _buildPengaduanSummary(BuildContext context) { Widget _buildPengaduanSummary(BuildContext context) {
return Obx(() { return Obx(() {
return Container( return Column(
width: double.infinity, children: [
padding: const EdgeInsets.all(16), Container(
decoration: BoxDecoration( width: double.infinity,
gradient: AppTheme.primaryGradient, padding: const EdgeInsets.all(16),
borderRadius: BorderRadius.circular(12), decoration: BoxDecoration(
), gradient: AppTheme.primaryGradient,
child: Column( borderRadius: BorderRadius.circular(12),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
), ),
const SizedBox(height: 16), child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Text(
child: _buildSummaryItem( 'Ringkasan Pengaduan',
context, style: Theme.of(context).textTheme.titleLarge?.copyWith(
icon: Icons.pending_actions, fontWeight: FontWeight.bold,
title: 'Diproses', color: Colors.white,
value: controller.jumlahDiproses.toString(), ),
color: Colors.orange,
),
), ),
Expanded( const SizedBox(height: 16),
child: _buildSummaryItem( Row(
context, children: [
icon: Icons.engineering, Expanded(
title: 'Tindakan', child: _buildSummaryItem(
value: controller.jumlahTindakan.toString(), context,
color: Colors.blue, icon: Icons.pending_actions,
), title: 'Diproses',
), value: controller.jumlahDiproses.toString(),
Expanded( color: Colors.orange,
child: _buildSummaryItem( ),
context, ),
icon: Icons.check_circle, Expanded(
title: 'Selesai', child: _buildSummaryItem(
value: controller.jumlahSelesai.toString(), context,
color: Colors.green, icon: Icons.engineering,
), title: 'Tindakan',
value: controller.jumlahTindakan.toString(),
color: Colors.blue,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.check_circle,
title: 'Selesai',
value: controller.jumlahSelesai.toString(),
color: Colors.green,
),
),
],
), ),
], ],
), ),
], ),
), ],
); );
}); });
} }
@ -215,6 +220,10 @@ class PengaduanView extends GetView<PengaduanController> {
value: 3, value: 3,
child: Text('Selesai'), child: Text('Selesai'),
), ),
const PopupMenuItem(
value: 4,
child: Text('Semua Kecuali Selesai'),
),
], ],
), ),
), ),

View File

@ -125,6 +125,21 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
); );
} }
// if 3
if (activeTab == 3) {
return Row(
children: [
IconButton(
onPressed: () {
Get.toNamed('/petugas-desa/riwayat-pengaduan');
},
icon: const Icon(Icons.history),
tooltip: 'Riwayat Pengaduan',
),
notificationButton,
],
);
}
return notificationButton; return notificationButton;
}), }),
], ],

View File

@ -0,0 +1,411 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
const RiwayatPengaduanView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Riwayat Pengaduan'),
),
body: RefreshIndicator(
onRefresh: controller.refreshData,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pencarian
_buildSearch(context),
// Informasi terakhir update
_buildLastUpdateInfo(context),
const SizedBox(height: 20),
// Daftar riwayat pengaduan
_buildRiwayatPengaduanList(context),
],
),
),
),
),
);
}
// Tambahkan widget untuk menampilkan waktu terakhir update
Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime.now();
final formattedDate = DateTimeHelper.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,
),
),
],
),
);
}
Widget _buildSearch(BuildContext context) {
return TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari riwayat pengaduan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
onChanged: (value) {
// Implementasi pencarian
controller.refreshData();
},
);
}
Widget _buildRiwayatPengaduanList(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
final filteredPengaduan = controller.getFilteredRiwayatPengaduan();
if (filteredPengaduan.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Icon(
Icons.inbox_outlined,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum ada riwayat pengaduan',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Riwayat Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
const SizedBox(height: 12),
...filteredPengaduan
.map((item) => _buildPengaduanItem(context, item)),
],
);
});
}
Widget _buildPengaduanItem(BuildContext context, dynamic item) {
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
} else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt);
}
return InkWell(
onTap: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.warga?['nama'] ?? item.judul ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle,
size: 16,
color: AppTheme.successColor,
),
const SizedBox(width: 4),
Text(
'SELESAI',
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.successColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
item.deskripsi ?? '',
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.person,
label: 'Pelapor',
value: item.warga?['nama_lengkap'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.numbers,
label: 'NIK',
value: item.warga?['nik'] ?? '',
),
),
],
),
const SizedBox(height: 12),
if (item.penerimaPenyaluran != null) ...[
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.shopping_bag,
label: 'Jumlah',
value:
'${item.jumlahBantuan} ${item.stokBantuan['satuan']}',
)),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Stok Bantuan',
value: item.stokBantuan['nama'] ?? '',
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.category,
label: 'Nama Penyaluran',
value: item.namaPenyaluran ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal',
value: formattedDate,
),
),
],
),
],
if (item.ratingWarga != null && item.ratingWarga > 0) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Feedback Warga',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.amber,
),
),
Row(
children: List.generate(5, (index) {
return Icon(
index < (item.ratingWarga ?? 0)
? Icons.star
: Icons.star_border,
color: Colors.amber,
size: 16,
);
}),
),
],
),
if (item.feedbackWarga != null &&
item.feedbackWarga.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'${item.feedbackWarga}',
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
),
],
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan',
arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}

View File

@ -13,6 +13,8 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart';
@ -142,5 +144,10 @@ class AppPages {
page: () => const WargaDetailPengaduanView(), page: () => const WargaDetailPengaduanView(),
binding: WargaBinding(), binding: WargaBinding(),
), ),
GetPage(
name: _Paths.riwayatPengaduan,
page: () => const RiwayatPengaduanView(),
binding: RiwayatPengaduanBinding(),
),
]; ];
} }

View File

@ -30,6 +30,7 @@ abstract class Routes {
static const wargaDetailPenerimaan = _Paths.wargaDetailPenerimaan; static const wargaDetailPenerimaan = _Paths.wargaDetailPenerimaan;
static const detailPengaduan = _Paths.detailPengaduan; static const detailPengaduan = _Paths.detailPengaduan;
static const wargaDetailPengaduan = _Paths.wargaDetailPengaduan; static const wargaDetailPengaduan = _Paths.wargaDetailPengaduan;
static const riwayatPengaduan = _Paths.riwayatPengaduan;
} }
abstract class _Paths { abstract class _Paths {
@ -62,4 +63,5 @@ abstract class _Paths {
static const wargaDetailPenerimaan = '/warga/detail-penerimaan'; static const wargaDetailPenerimaan = '/warga/detail-penerimaan';
static const detailPengaduan = '/detail-pengaduan'; static const detailPengaduan = '/detail-pengaduan';
static const wargaDetailPengaduan = '/warga/detail-pengaduan'; static const wargaDetailPengaduan = '/warga/detail-pengaduan';
static const riwayatPengaduan = '/petugas-desa/riwayat-pengaduan';
} }