Perbarui dependensi dengan menambahkan paket percent_indicator versi 4.2.4. Modifikasi file pubspec.yaml dan pubspec.lock untuk mencerminkan perubahan ini. Selain itu, perbarui status penerimaan di PelaksanaanPenyaluranController dari 'SUDAHMENERIMA' menjadi 'DITERIMA' untuk konsistensi. Tambahkan fungsionalitas baru di PetugasDesaDashboardController untuk memuat jadwal hari ini dan total penitipan terverifikasi. Perbarui tampilan di beberapa view untuk meningkatkan pengalaman pengguna dan konsistensi data.

This commit is contained in:
Khafidh Fuadi
2025-03-25 21:03:40 +07:00
parent 32736be867
commit 3b963178f4
20 changed files with 2191 additions and 818 deletions

View File

@ -1,101 +1,301 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/greeting_header.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/progress_section.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/schedule_card.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/cards/statistic_card.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
class DashboardView extends GetView<PetugasDesaController> {
class DashboardView extends GetView<PetugasDesaDashboardController> {
const DashboardView({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,
return RefreshIndicator(
onRefresh: () => controller.refreshData(),
child: Obx(() => AnimationLimiter(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: AnimationConfiguration.staggeredList(
position: 0,
delay: const Duration(milliseconds: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan greeting
FadeInAnimation(
child: GreetingHeader(
name: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.desa,
),
),
const SizedBox(height: 20),
// Jadwal penyaluran hari ini
FadeInAnimation(
delay: const Duration(milliseconds: 300),
child: _buildJadwalHariIni(),
),
const SizedBox(height: 20),
// Progress penyaluran
FadeInAnimation(
delay: const Duration(milliseconds: 400),
child: _buildProgressPenyaluran(),
),
const SizedBox(height: 20),
// Statistik performa desa
FadeInAnimation(
delay: const Duration(milliseconds: 500),
child: _buildStatistikPerforma(),
),
const SizedBox(height: 20),
// Daftar penerima terbaru
FadeInAnimation(
delay: const Duration(milliseconds: 600),
child: _buildRecipientsList(textTheme),
),
],
),
),
),
),
)),
);
}
Widget _buildJadwalHariIni() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 12),
FutureBuilder<List<Map<String, dynamic>>?>(
future: SupabaseService.to.getJadwalHariIni(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Gagal memuat jadwal'));
}
final jadwalList = snapshot.data;
if (jadwalList == null || jadwalList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.event_busy, color: Colors.grey),
SizedBox(width: 8),
Text('Tidak ada jadwal penyaluran hari ini'),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: jadwalList.length,
itemBuilder: (context, index) {
final jadwal = jadwalList[index];
final DateTime tanggal =
DateTime.parse(jadwal['tanggal_penyaluran']);
final String formattedDate =
DateTimeHelper.formatDateTime(tanggal);
final kategoriBantuan =
jadwal['kategori_bantuan'] as Map<String, dynamic>;
final lokasiPenyaluran =
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
return ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
);
},
);
},
),
],
);
}
Widget _buildProgressPenyaluran() {
// Menghitung nilai untuk progress
final terlaksana = controller.totalPenyaluran.value;
final total = controller.totalSemuaPenyaluran.value;
final progressValue = total > 0 ? terlaksana / total : 0.0;
final belumTerlaksana = total - terlaksana;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Progress Penyaluran',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
Row(
children: [
CircularPercentIndicator(
radius: 60.0,
lineWidth: 10.0,
percent: progressValue > 1.0 ? 1.0 : progressValue,
center: Text(
'${(progressValue * 100).toInt()}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
progressColor: Colors.white,
backgroundColor: Colors.white.withOpacity(0.2),
circularStrokeCap: CircularStrokeCap.round,
animation: true,
animationDuration: 1200,
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProgressDetailItem(
'Telah Terlaksana',
'$terlaksana',
Colors.white,
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Belum Terlaksana',
'$belumTerlaksana',
Colors.white.withOpacity(0.7),
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Total Penyaluran',
'$total',
Colors.white,
),
],
),
),
],
),
],
),
);
}
Widget _buildProgressDetailItem(String label, String value, Color color) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: color,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
Widget _buildStatistikPerforma() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistik Performa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 12),
Row(
children: [
// Header dengan greeting
GreetingHeader(
name: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.desa,
Expanded(
child: StatisticCard(
title: 'Penitipan',
count: controller.jumlahNotifikasiBelumDibaca.toString(),
subtitle: 'Perlu Konfirmasi',
height: 120,
icon: Icons.inbox,
),
),
const SizedBox(height: 20),
// Jadwal penyaluran hari ini
ScheduleCard(
title: 'Jadwal Penyaluran Hari ini',
location: 'Kantor Kepala Desa (Beras)',
dateTime: '15 April 2023, 13:00 - 14:00',
isToday: true,
onTap: () => Get.toNamed('/petugas-desa/jadwal'),
),
const SizedBox(height: 20),
// Jadwal penyaluran mendatang
ScheduleCard(
title: 'Jadwal Penyaluran Mendatang',
location: 'Balai Desa A (Sembako)',
dateTime: '17 April 2023, 13:00 - 14:00',
isToday: false,
onTap: () => Get.toNamed('/petugas-desa/jadwal'),
),
const SizedBox(height: 20),
// Statistik penyaluran
Row(
children: [
Expanded(
child: StatisticCard(
title: 'Penitipan',
count: '3',
subtitle: 'Perlu Konfirmasi',
height: 120,
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Pengaduan',
count:
'${controller.totalPenerima.value > 0 ? controller.totalPenerima.value ~/ 10 : 0}',
subtitle: 'Perlu Tindakan',
height: 120,
gradient: LinearGradient(
colors: [Colors.orange, Colors.deepOrange],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Penjadwalan',
count: '1',
subtitle: 'Perlu Konfirmasi',
height: 120,
),
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Pengaduan',
count: '1',
subtitle: 'Perlu Tindakan',
height: 120,
),
),
],
icon: Icons.warning_amber,
),
),
const SizedBox(height: 20),
// Progress penyaluran
ProgressSection(
progressValue: 0.7,
total: 100,
distributed: 70,
scheduled: 20,
unscheduled: 10,
),
const SizedBox(height: 20),
// Daftar penerima
_buildRecipientsList(textTheme),
],
),
),
],
);
}
@ -107,15 +307,16 @@ class DashboardView extends GetView<PetugasDesaController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Penerima',
style: textTheme.titleMedium?.copyWith(
fontSize: 16,
'Daftar Penerima Terbaru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
TextButton(
onPressed: () {
Get.toNamed('/daftar-penerima');
Get.toNamed(Routes.daftarPenerima);
},
child: Row(
children: [
@ -136,65 +337,121 @@ class DashboardView extends GetView<PetugasDesaController> {
],
),
const SizedBox(height: 10),
_buildRecipientItem(
'Siti Rahayu', '3201020107030011', 'Selesai', textTheme),
_buildRecipientItem(
'Budi Santoso', '3201020107030012', 'Selesai', textTheme),
_buildRecipientItem(
'Dewi Lestari', '3201020107030013', 'Selesai', textTheme),
FutureBuilder<List<Map<String, dynamic>>?>(
future: SupabaseService.to.getPenerimaTerbaru(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Gagal memuat data penerima'));
}
final penerimaList = snapshot.data;
if (penerimaList == null || penerimaList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.person_off, color: Colors.grey),
SizedBox(width: 8),
Text('Belum ada data penerima'),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: penerimaList.length > 3 ? 3 : penerimaList.length,
itemBuilder: (context, index) {
final penerima = penerimaList[index];
final name = penerima['nama_lengkap'] ?? 'Nama tidak tersedia';
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
final status = penerima['status'] ?? 'AKTIF';
final id = penerima['id'] ?? 'ID tidak tersedia';
return _buildRecipientItem(name, nik, status, id, textTheme);
},
);
},
),
],
);
}
Widget _buildRecipientItem(
String name, String nik, String status, TextTheme textTheme) {
String name, String nik, String status, String id, TextTheme textTheme) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: () {
// Navigasi ke detail penerima dengan ID statis
// Kita gunakan ID 1 untuk Siti Rahayu, 2 untuk Budi Santoso, 3 untuk Dewi Lestari
String id = "1"; // Default
if (nik == "3201020107030011") {
id = "2";
} else if (nik == "3201020107030012") {
id = "3";
}
Get.toNamed('/daftar-penerima/detail', arguments: id);
Get.toNamed(Routes.detailPenerima, arguments: id);
},
borderRadius: BorderRadius.circular(12),
child: ListTile(
title: Text(
name,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
subtitle: Text(
'NIK: $nik',
style: textTheme.bodyMedium?.copyWith(
color: Colors.white,
),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textTheme.bodySmall?.copyWith(
color: Colors.white,
fontSize: 12,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2),
child: const Icon(Icons.person, color: Colors.white),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'NIK: $nik',
style: textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textTheme.bodySmall?.copyWith(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
),
),