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

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