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

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