Tambahkan fitur pengaduan dan perbarui navigasi Petugas Desa

- Tambahkan kontroler untuk manajemen data pengaduan
- Buat tampilan PengaduanView untuk menampilkan daftar pengaduan
- Perbarui navigasi dengan menambahkan tab dan item baru untuk pengaduan
- Tambahkan logika untuk menghitung dan menampilkan jumlah pengaduan yang diproses
- Integrasikan fitur pengaduan ke dalam drawer dan bottom navigation bar
This commit is contained in:
Khafidh Fuadi
2025-03-08 22:13:12 +07:00
parent 45ff26e7f8
commit fca70143cd
14 changed files with 2540 additions and 440 deletions

View File

@ -6,6 +6,7 @@ class PetugasDesaBinding extends Bindings {
void dependencies() {
Get.lazyPut<PetugasDesaController>(
() => PetugasDesaController(),
fenix: true,
);
}
}

View File

@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
class JadwalSectionWidget extends StatelessWidget {
final PetugasDesaController controller;
final String title;
final List<Map<String, dynamic>> jadwalList;
final String status;
const JadwalSectionWidget({
Key? key,
required this.controller,
required this.title,
required this.jadwalList,
required this.status,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Obx(() {
final currentJadwalList = _getCurrentJadwalList();
if (currentJadwalList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.withAlpha(20),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'Tidak ada jadwal $title',
style: textTheme.titleMedium?.copyWith(
color: Colors.grey.shade600,
),
),
),
);
}
return Column(
children: currentJadwalList
.map((jadwal) => _buildJadwalItem(textTheme, jadwal))
.toList(),
);
}),
],
);
}
List<Map<String, dynamic>> _getCurrentJadwalList() {
switch (title) {
case 'Hari Ini':
return controller.jadwalHariIni;
case 'Mendatang':
return controller.jadwalMendatang;
case 'Selesai':
return controller.jadwalSelesai;
default:
return jadwalList;
}
}
Widget _buildJadwalItem(TextTheme textTheme, Map<String, dynamic> jadwal) {
Color statusColor;
switch (status) {
case 'Aktif':
statusColor = Colors.green;
break;
case 'Terjadwal':
statusColor = Colors.blue;
break;
case 'Selesai':
statusColor = Colors.grey;
break;
default:
statusColor = Colors.orange;
}
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
jadwal['lokasi'] ?? '',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Jenis Bantuan: ${jadwal['jenis_bantuan'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Tanggal: ${jadwal['tanggal'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Waktu: ${jadwal['waktu'] ?? ''}',
style: textTheme.bodyMedium,
),
if (jadwal['jumlah_penerima'] != null) ...[
const SizedBox(height: 4),
Text(
'Jumlah Penerima: ${jadwal['jumlah_penerima']}',
style: textTheme.bodyMedium,
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
final PetugasDesaController controller;
const PermintaanPenjadwalanSummaryWidget({
Key? key,
required this.controller,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Obx(() {
final jumlahPermintaan = controller.jumlahPermintaanPenjadwalan.value;
final permintaanList = controller.permintaanPenjadwalan;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(30),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: jumlahPermintaan > 0
? Colors.orange.withAlpha(50)
: Colors.grey.withAlpha(30),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Permintaan Penjadwalan',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: jumlahPermintaan > 0
? Colors.red.withAlpha(26)
: Colors.grey.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$jumlahPermintaan',
style: textTheme.bodySmall?.copyWith(
color: jumlahPermintaan > 0 ? Colors.red : Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
if (jumlahPermintaan == 0)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Icon(
Icons.event_note,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Tidak ada permintaan penjadwalan',
style: textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
)
else
Column(
children: [
...permintaanList.take(1).map((permintaan) =>
_buildPermintaanPreview(textTheme, permintaan)),
if (jumlahPermintaan > 1)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'+ ${jumlahPermintaan - 1} permintaan lainnya',
style: textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.permintaanPenjadwalan),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.visibility),
label: const Text('Lihat Semua Permintaan'),
),
),
],
),
);
});
}
Widget _buildPermintaanPreview(
TextTheme textTheme, Map<String, dynamic> permintaan) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.withAlpha(15),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
permintaan['nama'] ?? '',
style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Menunggu',
style: textTheme.bodySmall?.copyWith(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'Jenis: ${permintaan['jenis_bantuan'] ?? ''}',
style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
Text(
'Tanggal: ${permintaan['tanggal_permintaan'] ?? ''}',
style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@ -0,0 +1,348 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanWidget extends StatelessWidget {
final PetugasDesaController controller;
const PermintaanPenjadwalanWidget({
Key? key,
required this.controller,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Permintaan Penjadwalan',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Obx(() => Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${controller.jumlahPermintaanPenjadwalan.value}',
style: textTheme.bodySmall?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
)),
],
),
const SizedBox(height: 10),
Obx(() {
final permintaanList = controller.permintaanPenjadwalan;
// Jika tidak ada permintaan, tampilkan pesan kosong
if (permintaanList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.withAlpha(20),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
children: [
Icon(
Icons.event_note,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Tidak ada permintaan penjadwalan',
style: textTheme.titleMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
);
}
return Column(
children: permintaanList
.map(
(permintaan) => _buildPermintaanItem(textTheme, permintaan))
.toList(),
);
}),
],
);
}
// Widget untuk menampilkan item permintaan penjadwalan
Widget _buildPermintaanItem(
TextTheme textTheme, Map<String, dynamic> permintaan) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
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),
),
],
border: Border.all(
color: Colors.orange.withAlpha(50),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
permintaan['nama'] ?? '',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Menunggu',
style: textTheme.bodySmall?.copyWith(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'NIK: ${permintaan['nik'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Jenis Bantuan: ${permintaan['jenis_bantuan'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Tanggal Permintaan: ${permintaan['tanggal_permintaan'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Alamat: ${permintaan['alamat'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => _showTolakDialog(permintaan),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
child: const Text('Tolak'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _showKonfirmasiDialog(permintaan),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
child: const Text('Konfirmasi'),
),
],
),
],
),
),
);
}
// Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) {
String? selectedJadwalId;
// Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>(
value: jadwal['id'],
child: Text(
'${jadwal['tanggal']} - ${jadwal['lokasi']} (${jadwal['jenis_bantuan']})'),
);
}).toList();
// Tambahkan opsi jadwal lain jika diperlukan
jadwalOptions.add(
const DropdownMenuItem<String>(
value: '3',
child: Text('25 April 2023 - Kantor Kepala Desa (Beras)'),
),
);
Get.dialog(
AlertDialog(
title: const Text('Konfirmasi Permintaan'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama']}.'),
const SizedBox(height: 16),
const Text('Pilih jadwal penyaluran:'),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: jadwalOptions,
onChanged: (value) {
selectedJadwalId = value;
},
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (selectedJadwalId != null) {
// Panggil metode konfirmasi di controller
controller.konfirmasiPermintaanPenjadwalan(
permintaan['id'],
selectedJadwalId!,
);
Get.back();
Get.snackbar(
'Berhasil',
'Permintaan penjadwalan berhasil dikonfirmasi',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
Get.snackbar(
'Peringatan',
'Silakan pilih jadwal penyaluran terlebih dahulu',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
),
child: const Text('Konfirmasi'),
),
],
),
);
}
// Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) {
final TextEditingController alasanController = TextEditingController();
Get.dialog(
AlertDialog(
title: const Text('Tolak Permintaan'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama']}.'),
const SizedBox(height: 16),
const Text('Alasan penolakan:'),
const SizedBox(height: 8),
TextField(
controller: alasanController,
maxLines: 3,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Masukkan alasan penolakan',
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan(
permintaan['id'],
alasanController.text.trim(),
);
Get.back();
Get.snackbar(
'Berhasil',
'Permintaan penjadwalan berhasil ditolak',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
Get.snackbar(
'Peringatan',
'Silakan masukkan alasan penolakan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Tolak'),
),
],
),
);
}
}

View File

@ -31,6 +31,11 @@ class PetugasDesaController extends GetxController {
final RxList<Map<String, dynamic>> jadwalSelesai =
<Map<String, dynamic>>[].obs;
// Data untuk permintaan penjadwalan
final RxList<Map<String, dynamic>> permintaanPenjadwalan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahPermintaanPenjadwalan = 0.obs;
// Data untuk notifikasi
final RxList<Map<String, dynamic>> notifikasiBelumDibaca =
<Map<String, dynamic>>[].obs;
@ -50,6 +55,13 @@ class PetugasDesaController extends GetxController {
final RxInt jumlahTerverifikasi = 0.obs;
final RxInt jumlahDitolak = 0.obs;
// Data untuk pengaduan
final RxList<Map<String, dynamic>> daftarPengaduan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahDiproses = 0.obs;
final RxInt jumlahTindakan = 0.obs;
final RxInt jumlahSelesai = 0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
@ -60,12 +72,19 @@ class PetugasDesaController extends GetxController {
@override
void onInit() {
super.onInit();
// Inisialisasi manual untuk pengaduan (untuk debugging)
jumlahDiproses.value = 3;
print('onInit - Jumlah pengaduan diproses: ${jumlahDiproses.value}');
loadRoleData();
loadDashboardData();
loadJadwalData();
loadPermintaanPenjadwalanData();
loadNotifikasiData();
loadInventarisData();
loadPenitipanData();
loadPengaduanData();
}
@override
@ -187,6 +206,43 @@ class PetugasDesaController extends GetxController {
}
}
Future<void> loadPermintaanPenjadwalanData() async {
try {
// Simulasi data untuk permintaan penjadwalan
await Future.delayed(const Duration(milliseconds: 600));
permintaanPenjadwalan.value = [
{
'id': '1',
'nama': 'Ahmad Sulaiman',
'nik': '3201234567890001',
'jenis_bantuan': 'Beras',
'tanggal_permintaan': '14 April 2023',
'alamat': 'Dusun Sukamaju RT 02/03',
'status': 'menunggu',
},
{
'id': '2',
'nama': 'Siti Aminah',
'nik': '3201234567890002',
'jenis_bantuan': 'Sembako',
'tanggal_permintaan': '13 April 2023',
'alamat': 'Dusun Sukamaju RT 01/03',
'status': 'menunggu',
},
];
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPermintaanPenjadwalanData();
// permintaanPenjadwalan.value = result ?? [];
// jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
} catch (e) {
print('Error loading permintaan penjadwalan data: $e');
}
}
Future<void> loadNotifikasiData() async {
try {
// Simulasi data untuk notifikasi
@ -348,6 +404,126 @@ class PetugasDesaController extends GetxController {
}
}
Future<void> loadPengaduanData() async {
try {
// Simulasi data untuk pengaduan
await Future.delayed(const Duration(milliseconds: 650));
// Pastikan data pengaduan tidak kosong
daftarPengaduan.value = [
{
'id': '1',
'nama': 'Budi Santoso',
'nik': '3201020107030011',
'jenis_pengaduan': 'Bantuan Tidak Diterima',
'deskripsi':
'Saya belum menerima bantuan beras yang dijadwalkan minggu lalu',
'tanggal': '15 April 2023',
'status': 'Diproses',
},
{
'id': '2',
'nama': 'Siti Rahayu',
'nik': '3201020107030010',
'jenis_pengaduan': 'Kualitas Bantuan',
'deskripsi':
'Beras yang diterima berkualitas buruk dan tidak layak konsumsi',
'tanggal': '14 April 2023',
'status': 'Tindakan',
'tindakan':
'Pengecekan kualitas beras di gudang dan pengambilan sampel',
},
{
'id': '3',
'nama': 'Ahmad Fauzi',
'nik': '3201020107030013',
'jenis_pengaduan': 'Jumlah Bantuan',
'deskripsi':
'Jumlah bantuan yang diterima tidak sesuai dengan yang dijanjikan',
'tanggal': '13 April 2023',
'status': 'Tindakan',
'tindakan':
'Verifikasi data penerima dan jumlah bantuan yang seharusnya diterima',
},
{
'id': '4',
'nama': 'Dewi Lestari',
'nik': '3201020107030012',
'jenis_pengaduan': 'Jadwal Penyaluran',
'deskripsi':
'Jadwal penyaluran bantuan sering berubah tanpa pemberitahuan',
'tanggal': '10 April 2023',
'status': 'Selesai',
'tindakan':
'Koordinasi dengan tim penyaluran untuk perbaikan sistem pemberitahuan',
'hasil':
'Implementasi sistem notifikasi perubahan jadwal melalui SMS dan pengumuman di balai desa',
},
// Tambahkan data pengaduan dengan status 'Diproses' untuk memastikan counter muncul
{
'id': '5',
'nama': 'Joko Widodo',
'nik': '3201020107030014',
'jenis_pengaduan': 'Bantuan Tidak Sesuai',
'deskripsi':
'Bantuan yang diterima tidak sesuai dengan yang dijanjikan',
'tanggal': '16 April 2023',
'status': 'Diproses',
},
{
'id': '6',
'nama': 'Anita Sari',
'nik': '3201020107030015',
'jenis_pengaduan': 'Bantuan Tidak Tepat Sasaran',
'deskripsi':
'Bantuan diberikan kepada warga yang tidak berhak menerima',
'tanggal': '17 April 2023',
'status': 'Diproses',
},
];
// Hitung jumlah pengaduan berdasarkan status
int jumlahDiprosesTemp =
daftarPengaduan.where((p) => p['status'] == 'Diproses').length;
int jumlahTindakanTemp =
daftarPengaduan.where((p) => p['status'] == 'Tindakan').length;
int jumlahSelesaiTemp =
daftarPengaduan.where((p) => p['status'] == 'Selesai').length;
// Update nilai Rx
jumlahDiproses.value = jumlahDiprosesTemp;
jumlahTindakan.value = jumlahTindakanTemp;
jumlahSelesai.value = jumlahSelesaiTemp;
// Print untuk debugging
print('Data pengaduan dimuat:');
print('Jumlah pengaduan diproses: ${jumlahDiproses.value}');
print('Jumlah pengaduan tindakan: ${jumlahTindakan.value}');
print('Jumlah pengaduan selesai: ${jumlahSelesai.value}');
print('Total pengaduan: ${daftarPengaduan.length}');
// Perbarui UI secara manual
update();
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPengaduanData();
// daftarPengaduan.value = result ?? [];
// jumlahDiproses.value = daftarPengaduan.where((p) => p['status'] == 'Diproses').length;
// jumlahTindakan.value = daftarPengaduan.where((p) => p['status'] == 'Tindakan').length;
// jumlahSelesai.value = daftarPengaduan.where((p) => p['status'] == 'Selesai').length;
} catch (e) {
print('Error loading pengaduan data: $e');
}
}
// Method untuk memperbarui jumlah pengaduan secara manual (untuk debugging)
void updatePengaduanCounter() {
jumlahDiproses.value = 5; // Set nilai secara manual
update(); // Perbarui UI
print(
'Counter pengaduan diperbarui secara manual: ${jumlahDiproses.value}');
}
void tandaiNotifikasiDibaca(String id) {
// Implementasi untuk menandai notifikasi sebagai dibaca
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status notifikasi
@ -394,6 +570,24 @@ class PetugasDesaController extends GetxController {
loadPenitipanData();
}
void prosesPengaduan(String id, String tindakan) {
// Implementasi untuk memproses pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.processPengaduan(id, tindakan);
// Perbarui data lokal
loadPengaduanData();
}
void selesaikanPengaduan(String id, String hasil) {
// Implementasi untuk menyelesaikan pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.completePengaduan(id, hasil);
// Perbarui data lokal
loadPengaduanData();
}
void logout() {
_authController.logout();
}
@ -401,4 +595,82 @@ class PetugasDesaController extends GetxController {
void changeTab(int index) {
activeTabIndex.value = index;
}
// Metode untuk konfirmasi permintaan penjadwalan
Future<void> konfirmasiPermintaanPenjadwalan(
String id, String jadwalId) async {
try {
if (id.isEmpty || jadwalId.isEmpty) {
Get.snackbar(
'Error',
'ID permintaan atau jadwal tidak valid',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
// Simulasi proses konfirmasi
await Future.delayed(const Duration(milliseconds: 800));
// Hapus permintaan dari daftar
permintaanPenjadwalan.removeWhere((item) => item['id'] == id);
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diupdate ke Supabase
// await _supabaseService.konfirmasiPermintaanPenjadwalan(id, jadwalId);
// await loadPermintaanPenjadwalanData();
// await loadJadwalData();
} catch (e) {
print('Error konfirmasi permintaan penjadwalan: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat mengkonfirmasi permintaan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Metode untuk menolak permintaan penjadwalan
Future<void> tolakPermintaanPenjadwalan(String id, String alasan) async {
try {
if (id.isEmpty) {
Get.snackbar(
'Error',
'ID permintaan tidak valid',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
// Simulasi proses penolakan
await Future.delayed(const Duration(milliseconds: 800));
// Hapus permintaan dari daftar
permintaanPenjadwalan.removeWhere((item) => item['id'] == id);
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diupdate ke Supabase
// await _supabaseService.tolakPermintaanPenjadwalan(id, alasan);
// await loadPermintaanPenjadwalanData();
} catch (e) {
print('Error tolak permintaan penjadwalan: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat menolak permintaan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,289 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class JadwalView extends GetView<PetugasDesaController> {
const JadwalView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan jadwal
_buildJadwalSummary(context),
const SizedBox(height: 20),
// Jadwal hari ini
_buildJadwalSection(
textTheme,
title: 'Hari Ini',
jadwalList: [
{
'lokasi': 'Kantor Kepala Desa',
'jenisBantuan': 'Beras',
'tanggal': '15 April 2023',
'waktu': '13:00 - 14:00',
'status': 'Aktif',
},
],
),
const SizedBox(height: 20),
// Jadwal mendatang
_buildJadwalSection(
textTheme,
title: 'Mendatang',
jadwalList: [
{
'lokasi': 'Balai Desa A',
'jenisBantuan': 'Sembako',
'tanggal': '17 April 2023',
'waktu': '13:00 - 14:00',
'status': 'Terjadwal',
},
{
'lokasi': 'Balai Desa B',
'jenisBantuan': 'Uang Tunai',
'tanggal': '20 April 2023',
'waktu': '10:00 - 12:00',
'status': 'Terjadwal',
},
],
),
const SizedBox(height: 20),
// Jadwal selesai
_buildJadwalSection(
textTheme,
title: 'Selesai',
jadwalList: [
{
'lokasi': 'Kantor Kepala Desa',
'jenisBantuan': 'Beras',
'tanggal': '10 April 2023',
'waktu': '13:00 - 14:00',
'status': 'Selesai',
},
{
'lokasi': 'Balai Desa C',
'jenisBantuan': 'Sembako',
'tanggal': '5 April 2023',
'waktu': '09:00 - 11:00',
'status': 'Selesai',
},
],
),
],
),
),
);
}
Widget _buildJadwalSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Jadwal',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.pending_actions,
title: 'Terjadwal',
value: '5',
color: Colors.blue,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.event_available,
title: 'Aktif',
value: '1',
color: Colors.green,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.event_busy,
title: 'Selesai',
value: '12',
color: Colors.grey,
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required Color color,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildJadwalSection(
TextTheme textTheme, {
required String title,
required List<Map<String, String>> jadwalList,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
...jadwalList.map((jadwal) => _buildJadwalItem(textTheme, jadwal)),
],
);
}
Widget _buildJadwalItem(TextTheme textTheme, Map<String, String> jadwal) {
Color statusColor;
switch (jadwal['status']) {
case 'Aktif':
statusColor = Colors.green;
break;
case 'Terjadwal':
statusColor = Colors.blue;
break;
case 'Selesai':
statusColor = Colors.grey;
break;
default:
statusColor = Colors.orange;
}
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
jadwal['lokasi'] ?? '',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
jadwal['status'] ?? '',
style: textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Jenis Bantuan: ${jadwal['jenisBantuan'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Tanggal: ${jadwal['tanggal'] ?? ''}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 4),
Text(
'Waktu: ${jadwal['waktu'] ?? ''}',
style: textTheme.bodyMedium,
),
],
),
),
);
}
}

View File

@ -0,0 +1,649 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PengaduanView extends GetView<PetugasDesaController> {
const PengaduanView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan pengaduan
_buildPengaduanSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
const SizedBox(height: 20),
// Daftar pengaduan
_buildPengaduanList(context),
],
),
),
);
}
Widget _buildPengaduanSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.pending_actions,
title: 'Diproses',
value: '3',
color: Colors.orange,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.engineering,
title: 'Tindakan',
value: '2',
color: Colors.blue,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.check_circle,
title: 'Selesai',
value: '8',
color: Colors.green,
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required Color color,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Cari 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),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () {
// Tampilkan dialog filter
_showFilterDialog(context);
},
icon: const Icon(Icons.filter_list),
tooltip: 'Filter',
),
),
],
);
}
void _showFilterDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Filter Pengaduan'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CheckboxListTile(
title: const Text('Diproses'),
value: true,
onChanged: (value) {},
),
CheckboxListTile(
title: const Text('Tindakan'),
value: true,
onChanged: (value) {},
),
CheckboxListTile(
title: const Text('Selesai'),
value: true,
onChanged: (value) {},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Terapkan'),
),
],
),
);
}
Widget _buildPengaduanList(BuildContext context) {
final List<Map<String, dynamic>> pengaduanList = [
{
'id': '1',
'nama': 'Budi Santoso',
'nik': '3201020107030011',
'jenis_pengaduan': 'Bantuan Tidak Diterima',
'deskripsi':
'Saya belum menerima bantuan beras yang dijadwalkan minggu lalu',
'tanggal': '15 April 2023',
'status': 'Diproses',
},
{
'id': '2',
'nama': 'Siti Rahayu',
'nik': '3201020107030010',
'jenis_pengaduan': 'Kualitas Bantuan',
'deskripsi':
'Beras yang diterima berkualitas buruk dan tidak layak konsumsi',
'tanggal': '14 April 2023',
'status': 'Tindakan',
},
{
'id': '3',
'nama': 'Ahmad Fauzi',
'nik': '3201020107030013',
'jenis_pengaduan': 'Jumlah Bantuan',
'deskripsi':
'Jumlah bantuan yang diterima tidak sesuai dengan yang dijanjikan',
'tanggal': '13 April 2023',
'status': 'Tindakan',
},
{
'id': '4',
'nama': 'Dewi Lestari',
'nik': '3201020107030012',
'jenis_pengaduan': 'Jadwal Penyaluran',
'deskripsi':
'Jadwal penyaluran bantuan sering berubah tanpa pemberitahuan',
'tanggal': '10 April 2023',
'status': 'Selesai',
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Daftar Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...pengaduanList.map((item) => _buildPengaduanItem(context, item)),
],
);
}
Widget _buildPengaduanItem(BuildContext context, Map<String, dynamic> item) {
Color statusColor;
IconData statusIcon;
switch (item['status']) {
case 'Diproses':
statusColor = Colors.orange;
statusIcon = Icons.pending_actions;
break;
case 'Tindakan':
statusColor = Colors.blue;
statusIcon = Icons.engineering;
break;
case 'Selesai':
statusColor = Colors.green;
statusIcon = Icons.check_circle;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help_outline;
}
return 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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item['nama'] ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 16,
color: statusColor,
),
const SizedBox(width: 4),
Text(
item['status'] ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
'NIK: ${item['nik'] ?? ''}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
item['jenis_pengaduan'] ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 8),
Text(
item['deskripsi'] ?? '',
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.calendar_today,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
item['tanggal'] ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _buildActionButtons(context, item),
),
],
),
),
);
}
List<Widget> _buildActionButtons(
BuildContext context, Map<String, dynamic> item) {
final status = item['status'];
if (status == 'Diproses') {
return [
TextButton.icon(
onPressed: () {
// Implementasi untuk memproses pengaduan
_showTindakanDialog(context, item);
},
icon: const Icon(Icons.engineering, size: 18),
label: const Text('Tindakan'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Implementasi untuk melihat detail pengaduan
_showDetailDialog(context, item);
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} else if (status == 'Tindakan') {
return [
TextButton.icon(
onPressed: () {
// Implementasi untuk menyelesaikan pengaduan
_showSelesaikanDialog(context, item);
},
icon: const Icon(Icons.check_circle, size: 18),
label: const Text('Selesaikan'),
style: TextButton.styleFrom(
foregroundColor: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Implementasi untuk melihat detail pengaduan
_showDetailDialog(context, item);
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} else {
return [
TextButton.icon(
onPressed: () {
// Implementasi untuk melihat detail pengaduan
_showDetailDialog(context, item);
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
}
}
void _showDetailDialog(BuildContext context, Map<String, dynamic> item) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Detail Pengaduan: ${item['id']}'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailItem('Nama', item['nama'] ?? ''),
_buildDetailItem('NIK', item['nik'] ?? ''),
_buildDetailItem(
'Jenis Pengaduan', item['jenis_pengaduan'] ?? ''),
_buildDetailItem('Tanggal', item['tanggal'] ?? ''),
_buildDetailItem('Status', item['status'] ?? ''),
const SizedBox(height: 8),
const Text(
'Deskripsi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(item['deskripsi'] ?? ''),
if (item['status'] == 'Tindakan' ||
item['status'] == 'Selesai') ...[
const SizedBox(height: 8),
const Text(
'Tindakan:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(item['tindakan'] ??
'Pengecekan ke lokasi dan verifikasi data penerima'),
],
if (item['status'] == 'Selesai') ...[
const SizedBox(height: 8),
const Text(
'Hasil:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(item['hasil'] ??
'Pengaduan telah diselesaikan dengan penyaluran ulang bantuan'),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
],
),
);
}
Widget _buildDetailItem(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(child: Text(value)),
],
),
);
}
void _showTindakanDialog(BuildContext context, Map<String, dynamic> item) {
final TextEditingController tindakanController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tindakan Pengaduan'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item['nama']}'),
const SizedBox(height: 16),
TextField(
controller: tindakanController,
decoration: const InputDecoration(
labelText: 'Tindakan yang dilakukan',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
// Implementasi untuk menyimpan tindakan
Navigator.pop(context);
Get.snackbar(
'Berhasil',
'Status pengaduan berhasil diubah menjadi Tindakan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.blue,
colorText: Colors.white,
);
},
child: const Text('Simpan'),
),
],
),
);
}
void _showSelesaikanDialog(BuildContext context, Map<String, dynamic> item) {
final TextEditingController hasilController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Selesaikan Pengaduan'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item['nama']}'),
const SizedBox(height: 16),
TextField(
controller: hasilController,
decoration: const InputDecoration(
labelText: 'Hasil penyelesaian',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
// Implementasi untuk menyimpan hasil
Navigator.pop(context);
Get.snackbar(
'Berhasil',
'Status pengaduan berhasil diubah menjadi Selesai',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
},
child: const Text('Selesaikan'),
),
],
),
);
}
}

View File

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart';
class PenyaluranView extends GetView<PetugasDesaController> {
const PenyaluranView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan jadwal
_buildJadwalSummary(context),
const SizedBox(height: 20),
// Ringkasan Permintaan Penjadwalan
PermintaanPenjadwalanSummaryWidget(controller: controller),
const SizedBox(height: 20),
// Jadwal hari ini
JadwalSectionWidget(
controller: controller,
title: 'Hari Ini',
jadwalList: controller.jadwalHariIni,
status: 'Aktif',
),
const SizedBox(height: 20),
// Jadwal mendatang
JadwalSectionWidget(
controller: controller,
title: 'Mendatang',
jadwalList: controller.jadwalMendatang,
status: 'Terjadwal',
),
const SizedBox(height: 20),
// Jadwal selesai
JadwalSectionWidget(
controller: controller,
title: 'Selesai',
jadwalList: controller.jadwalSelesai,
status: 'Selesai',
),
],
),
),
);
}
Widget _buildJadwalSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Jadwal',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Obx(() => _buildSummaryItem(
context,
icon: Icons.pending_actions,
title: 'Terjadwal',
value: '${controller.jadwalMendatang.length}',
color: Colors.blue,
)),
),
Expanded(
child: Obx(() => _buildSummaryItem(
context,
icon: Icons.event_available,
title: 'Aktif',
value: '${controller.jadwalHariIni.length}',
color: Colors.green,
)),
),
Expanded(
child: Obx(() => _buildSummaryItem(
context,
icon: Icons.event_busy,
title: 'Selesai',
value: '${controller.jadwalSelesai.length}',
color: Colors.grey,
)),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required Color color,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
}

View File

@ -0,0 +1,341 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
const PermintaanPenjadwalanView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Pastikan controller sudah diinisialisasi
if (!Get.isRegistered<PetugasDesaController>()) {
Get.put(PetugasDesaController());
}
return Scaffold(
appBar: AppBar(
title: const Text('Permintaan Penjadwalan'),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Get.back(),
),
),
body: Obx(() {
final permintaanList = controller.permintaanPenjadwalan;
if (permintaanList.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: permintaanList.length,
itemBuilder: (context, index) {
final permintaan = permintaanList[index];
return _buildPermintaanItem(context, permintaan);
},
);
}),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event_note,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Tidak ada permintaan penjadwalan',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Semua permintaan penjadwalan akan muncul di sini',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildPermintaanItem(
BuildContext context, Map<String, dynamic> permintaan) {
final textTheme = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.orange.withAlpha(50),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
permintaan['nama'] ?? '',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Menunggu',
style: textTheme.bodySmall?.copyWith(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
_buildInfoRow(Icons.person, 'NIK: ${permintaan['nik'] ?? ''}'),
_buildInfoRow(Icons.category,
'Jenis Bantuan: ${permintaan['jenis_bantuan'] ?? ''}'),
_buildInfoRow(Icons.calendar_today,
'Tanggal Permintaan: ${permintaan['tanggal_permintaan'] ?? ''}'),
_buildInfoRow(
Icons.location_on, 'Alamat: ${permintaan['alamat'] ?? ''}'),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
onPressed: () => _showTolakDialog(permintaan),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
icon: const Icon(Icons.close),
label: const Text('Tolak'),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: () => _showKonfirmasiDialog(permintaan),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
icon: const Icon(Icons.check),
label: const Text('Konfirmasi'),
),
],
),
],
),
),
);
}
Widget _buildInfoRow(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade800,
),
),
),
],
),
);
}
// Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) {
String? selectedJadwalId;
// Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>(
value: jadwal['id'],
child: Text(
'${jadwal['tanggal'] ?? ''} - ${jadwal['lokasi'] ?? ''} (${jadwal['jenis_bantuan'] ?? ''})'),
);
}).toList();
// Tambahkan opsi jadwal lain jika diperlukan
jadwalOptions.add(
const DropdownMenuItem<String>(
value: '3',
child: Text('25 April 2023 - Kantor Kepala Desa (Beras)'),
),
);
Get.dialog(
AlertDialog(
title: const Text('Konfirmasi Permintaan'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'),
const SizedBox(height: 16),
const Text('Pilih jadwal penyaluran:'),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: jadwalOptions,
onChanged: (value) {
selectedJadwalId = value;
},
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (selectedJadwalId != null) {
// Panggil metode konfirmasi di controller
controller.konfirmasiPermintaanPenjadwalan(
permintaan['id'] ?? '',
selectedJadwalId ?? '',
);
Get.back();
Get.snackbar(
'Berhasil',
'Permintaan penjadwalan berhasil dikonfirmasi',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
Get.snackbar(
'Peringatan',
'Silakan pilih jadwal penyaluran terlebih dahulu',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
),
child: const Text('Konfirmasi'),
),
],
),
);
}
// Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) {
final TextEditingController alasanController = TextEditingController();
Get.dialog(
AlertDialog(
title: const Text('Tolak Permintaan'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'),
const SizedBox(height: 16),
const Text('Alasan penolakan:'),
const SizedBox(height: 8),
TextField(
controller: alasanController,
maxLines: 3,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Masukkan alasan penolakan',
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan(
permintaan['id'] ?? '',
alasanController.text.trim(),
);
Get.back();
Get.snackbar(
'Berhasil',
'Permintaan penjadwalan berhasil ditolak',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
Get.snackbar(
'Peringatan',
'Silakan masukkan alasan penolakan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Tolak'),
),
],
),
);
}
}

View File

@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/jadwal_view.dart';
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/inventaris_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';
class PetugasDesaView extends GetView<PetugasDesaController> {
@ -15,6 +16,12 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
// Perbarui counter pengaduan secara manual saat aplikasi dimulai
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.updatePengaduanCounter();
print('Counter pengaduan diperbarui saat aplikasi dimulai');
});
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
@ -23,11 +30,13 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 0:
return const Text('Dashboard');
case 1:
return const Text('Jadwal Penyaluran');
return const Text('Penyaluran');
case 2:
return const Text('Inventaris');
case 3:
return const Text('Penitipan');
case 3:
return const Text('Pengaduan');
case 4:
return const Text('Inventaris');
default:
return const Text('Petugas Desa');
}
@ -115,6 +124,35 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
notificationButton,
],
);
} else if (activeTab == 4) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Tambah Pengaduan',
onPressed: () {
// Implementasi untuk menambah pengaduan baru
},
),
IconButton(
icon: const Icon(Icons.filter_list),
tooltip: 'Filter Pengaduan',
onPressed: () {
// Implementasi untuk filter pengaduan
},
),
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Perbarui Counter',
onPressed: () {
// Perbarui counter pengaduan secara manual
controller.updatePengaduanCounter();
},
),
notificationButton,
],
);
} else {
return notificationButton;
}
@ -127,11 +165,14 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 0:
return const DashboardView();
case 1:
return const JadwalView();
return const PenyaluranView();
case 2:
return const InventarisView();
case 3:
return const PenitipanView();
case 3:
return const PengaduanView();
case 4:
return const InventarisView();
default:
return const DashboardView();
}
@ -193,7 +234,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
)),
Obx(() => ListTile(
leading: const Icon(Icons.calendar_today_outlined),
title: const Text('Jadwal Penyaluran'),
title: const Text('Penyaluran'),
selected: controller.activeTabIndex.value == 1,
selectedColor: AppTheme.primaryColor,
onTap: () {
@ -201,16 +242,6 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Navigator.pop(context);
},
)),
Obx(() => ListTile(
leading: const Icon(Icons.inventory_2_outlined),
title: const Text('Inventaris'),
selected: controller.activeTabIndex.value == 2,
selectedColor: AppTheme.primaryColor,
onTap: () {
controller.changeTab(2);
Navigator.pop(context);
},
)),
Obx(() => ListTile(
leading: Stack(
alignment: Alignment.center,
@ -243,10 +274,65 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
],
),
title: const Text('Penitipan'),
selected: controller.activeTabIndex.value == 3,
selected: controller.activeTabIndex.value == 2,
selectedColor: AppTheme.primaryColor,
onTap: () {
controller.changeTab(3);
controller.changeTab(2);
Navigator.pop(context);
},
)),
Obx(() {
final int jumlahPengaduanDiproses = controller.jumlahDiproses.value;
print(
'Drawer - Jumlah pengaduan diproses: $jumlahPengaduanDiproses');
return ListTile(
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.report_problem_outlined),
// Selalu tampilkan badge untuk debugging
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
jumlahPengaduanDiproses.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
),
],
),
title: const Text('Pengaduan'),
selected: controller.activeTabIndex.value == 3,
selectedColor: AppTheme.primaryColor,
onTap: () {
controller.changeTab(3);
Navigator.pop(context);
},
);
}),
Obx(() => ListTile(
leading: const Icon(Icons.inventory_2_outlined),
title: const Text('Inventaris'),
selected: controller.activeTabIndex.value == 4,
selectedColor: AppTheme.primaryColor,
onTap: () {
controller.changeTab(4);
Navigator.pop(context);
},
)),
@ -323,150 +409,222 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
}
Widget _buildBottomNavigationBar() {
return Obx(() => BottomNavigationBar(
currentIndex: controller.activeTabIndex.value,
onTap: controller.changeTab,
type: BottomNavigationBarType.fixed,
selectedItemColor: AppTheme.primaryColor,
unselectedItemColor: Colors.grey,
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.dashboard_outlined),
activeIcon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.calendar_today_outlined),
if (controller.jadwalHariIni.isNotEmpty)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jadwalHariIni.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
// Tambahkan print statement untuk debugging
print('Jumlah pengaduan diproses: ${controller.jumlahDiproses.value}');
print('Jumlah jadwal hari ini: ${controller.jadwalHariIni.length}');
return Obx(() {
// Hitung jumlah pengaduan yang diproses
final int jumlahPengaduanDiproses = controller.jumlahDiproses.value;
return BottomNavigationBar(
currentIndex: controller.activeTabIndex.value,
onTap: controller.changeTab,
type: BottomNavigationBarType.fixed,
selectedItemColor: AppTheme.primaryColor,
unselectedItemColor: Colors.grey,
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.dashboard_outlined),
activeIcon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.calendar_today_outlined),
if (controller.jadwalHariIni.isNotEmpty)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jadwalHariIni.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
],
),
activeIcon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.calendar_today),
if (controller.jadwalHariIni.isNotEmpty)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jadwalHariIni.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
],
),
activeIcon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.calendar_today),
if (controller.jadwalHariIni.isNotEmpty)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jadwalHariIni.length.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
],
),
label: 'Jadwal',
),
],
),
const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2),
label: 'Inventaris',
),
BottomNavigationBarItem(
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.handshake_outlined),
if (controller.jumlahMenunggu.value > 0)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahMenunggu.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
label: 'Penyaluran',
),
BottomNavigationBarItem(
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.handshake_outlined),
if (controller.jumlahMenunggu.value > 0)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahMenunggu.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
],
),
activeIcon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.handshake),
if (controller.jumlahMenunggu.value > 0)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahMenunggu.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
],
),
activeIcon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.handshake),
if (controller.jumlahMenunggu.value > 0)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahMenunggu.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
],
),
label: 'Penitipan',
),
],
),
],
));
label: 'Penitipan',
),
BottomNavigationBarItem(
icon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.report_problem_outlined),
// Selalu tampilkan badge untuk debugging
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahDiproses.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
),
],
),
activeIcon: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.report_problem),
// Selalu tampilkan badge untuk debugging
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
controller.jumlahDiproses.value.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
),
textAlign: TextAlign.center,
),
),
),
],
),
label: 'Pengaduan',
),
const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2),
label: 'Inventaris',
),
],
);
});
}
}

View File

@ -0,0 +1,8 @@
import 'package:get/get.dart';
class SplashBinding extends Bindings {
@override
void dependencies() {
// Tidak perlu menambahkan controller khusus untuk splash screen
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class SplashView extends StatefulWidget {
const SplashView({Key? key}) : super(key: key);
@override
State<SplashView> createState() => _SplashViewState();
}
class _SplashViewState extends State<SplashView> {
@override
void initState() {
super.initState();
_navigateToLogin();
}
_navigateToLogin() async {
await Future.delayed(const Duration(seconds: 2));
Get.offAllNamed(Routes.login);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 120,
height: 120,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.people,
size: 60,
color: AppTheme.primaryColor,
),
);
},
),
const SizedBox(height: 24),
const Text(
'Aplikasi Penyaluran',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'Bantuan Sosial',
style: TextStyle(
fontSize: 18,
color: Colors.white,
),
),
const SizedBox(height: 48),
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
],
),
),
),
);
}
}

View File

@ -9,6 +9,7 @@ import 'package:penyaluran_app/app/modules/home/bindings/home_binding.dart';
import 'package:penyaluran_app/app/modules/dashboard/bindings/dashboard_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/petugas_desa_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/petugas_desa_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart';
part 'app_routes.dart';
@ -48,5 +49,10 @@ class AppPages {
page: () => const DonaturDashboardView(),
binding: DashboardBinding(),
),
GetPage(
name: _Paths.permintaanPenjadwalan,
page: () => const PermintaanPenjadwalanView(),
binding: PetugasDesaBinding(),
),
];
}

View File

@ -9,6 +9,8 @@ abstract class Routes {
static const petugasVerifikasiDashboard = _Paths.petugasVerifikasiDashboard;
static const petugasDesaDashboard = _Paths.petugasDesaDashboard;
static const donaturDashboard = _Paths.donaturDashboard;
static const splash = _Paths.splash;
static const permintaanPenjadwalan = _Paths.permintaanPenjadwalan;
}
abstract class _Paths {
@ -20,4 +22,6 @@ abstract class _Paths {
static const petugasVerifikasiDashboard = '/petugas-verifikasi-dashboard';
static const petugasDesaDashboard = '/petugas-desa-dashboard';
static const donaturDashboard = '/donatur-dashboard';
static const splash = '/splash';
static const permintaanPenjadwalan = '/permintaan-penjadwalan';
}