Perbarui beberapa file konfigurasi fingerprint untuk arsitektur arm64-v8a, armeabi-v7a, x86, dan x86_64. Modifikasi tampilan dan controller di modul donatur dan petugas desa untuk meningkatkan pengalaman pengguna, termasuk penggantian logika pengambilan data dan penyesuaian tampilan. Hapus kode yang tidak digunakan dan tambahkan fungsionalitas baru untuk mendukung pengelolaan data yang lebih baik.

This commit is contained in:
Khafidh Fuadi
2025-03-27 16:55:56 +07:00
parent f74c058c71
commit f6d3eef2cf
31 changed files with 3372 additions and 1339 deletions

View File

@ -8,11 +8,16 @@ import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class CalendarViewWidget extends StatelessWidget {
final JadwalPenyaluranController controller;
final CalendarController _calendarController = CalendarController();
const CalendarViewWidget({
CalendarViewWidget({
super.key,
required this.controller,
});
}) {
// Mengatur controller kalender untuk selalu memilih hari ini saat inisialisasi
_calendarController.selectedDate = DateTime.now();
_calendarController.displayDate = DateTime.now();
}
@override
Widget build(BuildContext context) {
@ -49,6 +54,9 @@ class CalendarViewWidget extends StatelessWidget {
child: Obx(() {
return SfCalendar(
view: CalendarView.month,
controller: _calendarController,
initialSelectedDate: DateTime.now(),
initialDisplayDate: DateTime.now(),
dataSource: _getCalendarDataSource(),
timeZone: 'Asia/Jakarta',
monthViewSettings: MonthViewSettings(
@ -246,7 +254,7 @@ class CalendarViewWidget extends StatelessWidget {
List<Appointment> appointments = [];
List<PenyaluranBantuanModel> allJadwal = [
...controller.jadwalHariIni,
...controller.jadwalAktif,
...controller.jadwalMendatang,
...controller.jadwalTerlaksana,
];
@ -556,7 +564,7 @@ class CalendarViewWidget extends StatelessWidget {
// Cari jadwal dengan ID yang sesuai
for (var jadwal in [
...controller.jadwalHariIni,
...controller.jadwalAktif,
...controller.jadwalMendatang,
...controller.jadwalTerlaksana
]) {

View File

@ -165,9 +165,10 @@ class JadwalSectionWidget extends StatelessWidget {
List<PenyaluranBantuanModel> _getCurrentJadwalList() {
switch (title) {
case 'Hari Ini':
return controller.jadwalHariIni.toList();
case 'Mendatang':
case 'Penyaluran Aktif':
return controller.jadwalAktif.toList();
case '7 Hari Mendatang':
return controller.jadwalMendatang.toList();
case 'Terlaksana':
return controller.jadwalTerlaksana.toList();

View File

@ -73,17 +73,23 @@ class CounterService extends GetxService {
void updatePengaduanCounter(int diproses) {
jumlahDiproses.value = diproses;
_storage.write(_keyDiproses, diproses);
print('Counter pengaduan updated and saved - Diproses: $diproses');
}
// Metode untuk memperbarui counter notifikasi
void updateNotifikasiCounter(int belumDibaca) {
jumlahNotifikasiBelumDibaca.value = belumDibaca;
_storage.write(_keyNotifikasi, belumDibaca);
print('Counter notifikasi updated and saved - Belum Dibaca: $belumDibaca');
}
// Metode untuk memperbarui counter jadwal
void updateJadwalCounter(int hariIni) {
jumlahJadwalHariIni.value = hariIni;
_storage.write(_keyJadwal, hariIni);
print('Counter jadwal updated and saved - Hari Ini: $hariIni');
}
}

View File

@ -204,6 +204,27 @@ class DetailPenyaluranController extends GetxController {
.update(updateData)
.eq('id', penerima.id!);
// Dapatkan data penerima penyaluran (stok_bantuan_id dan jumlah)
final penerimaData = await _supabaseService.client
.from('penerima_penyaluran')
.select('penyaluran_bantuan_id, stok_bantuan_id, jumlah_bantuan')
.eq('id', penerima.id!)
.single();
if (penerimaData != null) {
final String stokBantuanId = penerimaData['stok_bantuan_id'];
final double jumlah = penerimaData['jumlah_bantuan'] is int
? penerimaData['jumlah_bantuan'].toDouble()
: penerimaData['jumlah_bantuan'];
// Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId);
}
}
// Refresh data setelah konfirmasi berhasil
await refreshData();

View File

@ -24,7 +24,7 @@ class JadwalPenyaluranController extends GetxController {
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk jadwal
final RxList<PenyaluranBantuanModel> jadwalHariIni =
final RxList<PenyaluranBantuanModel> jadwalAktif =
<PenyaluranBantuanModel>[].obs;
final RxList<PenyaluranBantuanModel> jadwalMendatang =
<PenyaluranBantuanModel>[].obs;
@ -97,7 +97,7 @@ class JadwalPenyaluranController extends GetxController {
List<PenyaluranBantuanModel> jadwalToUpdate = [];
List<PenyaluranBantuanModel> jadwalTerlewat = [];
for (var jadwal in jadwalHariIni) {
for (var jadwal in jadwalAktif) {
if (jadwal.tanggalPenyaluran != null) {
final jadwalDateTime =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
@ -175,9 +175,9 @@ class JadwalPenyaluranController extends GetxController {
isLoading.value = true;
try {
// Mengambil data jadwal hari ini
final jadwalHariIniData = await _supabaseService.getJadwalHariIni();
if (jadwalHariIniData != null) {
jadwalHariIni.value = jadwalHariIniData
final jadwalAktifData = await _supabaseService.getJadwalAktif();
if (jadwalAktifData != null) {
jadwalAktif.value = jadwalAktifData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}

View File

@ -250,29 +250,29 @@ class PelaksanaanPenyaluranController extends GetxController {
filteredPenerima.value = filtered;
}
// Metode untuk memperbarui status penerimaan bantuan
Future<bool> updateStatusPenerimaan(int penerimaId, String status,
{DateTime? tanggalPenerimaan,
String? buktiPenerimaan,
String? keterangan}) async {
try {
final result = await supabaseService.updateStatusPenerimaan(
penerimaId, status,
tanggalPenerimaan: tanggalPenerimaan,
buktiPenerimaan: buktiPenerimaan,
keterangan: keterangan);
// // Metode untuk memperbarui status penerimaan bantuan
// Future<bool> updateStatusPenerimaan(int penerimaId, String status,
// {DateTime? tanggalPenerimaan,
// String? buktiPenerimaan,
// String? keterangan}) async {
// try {
// final result = await supabaseService.updateStatusPenerimaan(
// penerimaId, status,
// tanggalPenerimaan: tanggalPenerimaan,
// buktiPenerimaan: buktiPenerimaan,
// keterangan: keterangan);
// Jika berhasil, perbarui data lokal
if (result) {
await loadPenerimaPenyaluran(activePenyaluranId.value);
}
// // Jika berhasil, perbarui data lokal
// if (result) {
// await loadPenerimaPenyaluran(activePenyaluranId.value);
// }
return result;
} catch (e) {
print('Error updating status penerimaan: $e');
return false;
}
}
// return result;
// } catch (e) {
// print('Error updating status penerimaan: $e');
// return false;
// }
// }
// Metode untuk menyelesaikan jadwal penyaluran
Future<void> completeJadwal(String jadwalId) async {

View File

@ -309,10 +309,10 @@ class PetugasDesaController extends GetxController {
// Metode untuk memuat data jadwal
Future<void> loadJadwalData() async {
try {
final jadwalHariIniData = await _supabaseService.getJadwalHariIni();
if (jadwalHariIniData != null) {
jadwalHariIni.value = jadwalHariIniData;
_counterService.updateJadwalCounter(jadwalHariIniData.length);
final jadwalAktifData = await _supabaseService.getJadwalAktif();
if (jadwalAktifData != null) {
jadwalHariIni.value = jadwalAktifData;
_counterService.updateJadwalCounter(jadwalAktifData.length);
}
} catch (e) {
print('Error loading jadwal data: $e');
@ -360,7 +360,7 @@ class PetugasDesaController extends GetxController {
// Hitung jumlah pengaduan dengan status DIPROSES
for (var item in pengaduanData) {
if (item['status'] == 'DIPROSES') {
if (item['status'] == 'MENUNGGU') {
diproses++;
}
}
@ -609,22 +609,22 @@ class PetugasDesaController extends GetxController {
}
// Metode untuk memperbarui status penerimaan bantuan
Future<bool> updateStatusPenerimaan(int penerimaId, String status,
{DateTime? tanggalPenerimaan,
String? buktiPenerimaan,
String? keterangan}) async {
try {
final result = await _supabaseService.updateStatusPenerimaan(
penerimaId, status,
tanggalPenerimaan: tanggalPenerimaan,
buktiPenerimaan: buktiPenerimaan,
keterangan: keterangan);
return result;
} catch (e) {
print('Error updating status penerimaan: $e');
return false;
}
}
// Future<bool> updateStatusPenerimaan(int penerimaId, String status,
// {DateTime? tanggalPenerimaan,
// String? buktiPenerimaan,
// String? keterangan}) async {
// try {
// final result = await _supabaseService.updateStatusPenerimaan(
// penerimaId, status,
// tanggalPenerimaan: tanggalPenerimaan,
// buktiPenerimaan: buktiPenerimaan,
// keterangan: keterangan);
// return result;
// } catch (e) {
// print('Error updating status penerimaan: $e');
// return false;
// }
// }
// Metode untuk menyelesaikan jadwal penyaluran
Future<void> completeJadwal(String jadwalId) async {

View File

@ -4,10 +4,12 @@ import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
class PetugasDesaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
late final CounterService _counterService;
final RxBool isLoading = false.obs;
@ -22,6 +24,12 @@ class PetugasDesaDashboardController extends GetxController {
final RxInt totalPenitipanTerverifikasi = 0.obs;
final RxDouble progressPenyaluran = 0.0.obs;
// Data untuk status penyaluran
final RxInt penyaluranDijadwalkan = 0.obs;
final RxInt penyaluranAktif = 0.obs;
final RxInt penyaluranBatal = 0.obs;
final RxInt penyaluranTerlaksana = 0.obs;
// Data untuk notifikasi
final RxList<NotifikasiModel> notifikasiBelumDibaca = <NotifikasiModel>[].obs;
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
@ -45,13 +53,24 @@ class PetugasDesaDashboardController extends GetxController {
userProfile['desa']?['nama'] ??
(userProfile['desa_id'] != null ? 'Desa' : 'Desa');
// Getter untuk counter dari CounterService
RxInt get jumlahMenunggu => _counterService.jumlahMenunggu;
RxInt get jumlahDiproses => _counterService.jumlahDiproses;
@override
void onInit() {
super.onInit();
// Inisialisasi CounterService jika belum ada
if (!Get.isRegistered<CounterService>()) {
Get.put(CounterService(), permanent: true);
}
_counterService = Get.find<CounterService>();
loadUserProfile();
loadDashboardData();
loadNotifikasiData();
loadJadwalHariIni();
loadJadwalAktif();
}
@override
@ -97,6 +116,15 @@ class PetugasDesaDashboardController extends GetxController {
await _supabaseService.getTotalSemuaPenyaluran();
totalSemuaPenyaluran.value = semuaPenyaluranData ?? 0;
// Mengambil data status penyaluran
final statusPenyaluranData = await _supabaseService.getStatusPenyaluran();
if (statusPenyaluranData != null) {
penyaluranDijadwalkan.value = statusPenyaluranData['dijadwalkan'] ?? 0;
penyaluranAktif.value = statusPenyaluranData['aktif'] ?? 0;
penyaluranBatal.value = statusPenyaluranData['batal'] ?? 0;
penyaluranTerlaksana.value = statusPenyaluranData['terlaksana'] ?? 0;
}
// Menghitung progress penyaluran (persentase penyaluran yang terlaksana dari total semua penyaluran)
if (totalSemuaPenyaluran.value > 0) {
progressPenyaluran.value =
@ -127,9 +155,9 @@ class PetugasDesaDashboardController extends GetxController {
}
}
Future<void> loadJadwalHariIni() async {
Future<void> loadJadwalAktif() async {
try {
final jadwalData = await _supabaseService.getJadwalHariIni();
final jadwalData = await _supabaseService.getJadwalAktif();
if (jadwalData != null) {
jadwalHariIni.value = jadwalData;
}
@ -145,7 +173,7 @@ class PetugasDesaDashboardController extends GetxController {
loadUserProfile(),
loadDashboardData(),
loadNotifikasiData(),
loadJadwalHariIni(),
loadJadwalAktif(),
]);
} catch (e) {
print('Error refreshing data: $e');

View File

@ -302,4 +302,26 @@ class RiwayatStokController extends GetxController {
alasan.value = '';
fotoBukti.value = null;
}
// Metode untuk mendapatkan detail referensi berdasarkan id dan sumber
Future<Map<String, dynamic>?> getReferensiDetail({
required String idReferensi,
required String sumber,
}) async {
try {
Map<String, dynamic>? data;
// Berdasarkan sumber, ambil data dari tabel yang sesuai
if (sumber == 'penitipan') {
data = await _supabaseService.getPenitipanById(idReferensi);
} else if (sumber == 'penerimaan') {
data = await _supabaseService.getPenerimaanById(idReferensi);
}
return data;
} catch (e) {
print('Error getting referensi detail: $e');
throw Exception('Gagal mendapatkan data: $e');
}
}
}

View File

@ -92,7 +92,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
),
const SizedBox(height: 12),
FutureBuilder<List<Map<String, dynamic>>?>(
future: SupabaseService.to.getJadwalHariIni(),
future: SupabaseService.to.getJadwalAktif(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
@ -153,11 +153,16 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
}
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;
// Menghitung nilai untuk progress berdasarkan status
final terlaksana = controller.penyaluranTerlaksana.value;
final batal = controller.penyaluranBatal.value;
final dijadwalkan = controller.penyaluranDijadwalkan.value;
final aktif = controller.penyaluranAktif.value;
final total = terlaksana + batal + dijadwalkan + aktif;
final progressValue = total > 0 ? (terlaksana + batal) / total : 0.0;
final belumTerlaksana = dijadwalkan +
aktif; // Yang belum terlaksana adalah yang dijadwalkan dan aktif
return Container(
padding: const EdgeInsets.all(16),
@ -214,6 +219,12 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
Colors.white.withOpacity(0.7),
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Dibatalkan',
'$batal',
Colors.white.withOpacity(0.7),
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Total Penyaluran',
'$total',
@ -270,18 +281,22 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
Expanded(
child: StatisticCard(
title: 'Penitipan',
count: controller.jumlahNotifikasiBelumDibaca.toString(),
count: controller.jumlahMenunggu.value.toString(),
subtitle: 'Perlu Konfirmasi',
height: 120,
icon: Icons.inbox,
gradient: LinearGradient(
colors: [Colors.orange, Colors.deepOrange],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Pengaduan',
count:
'${controller.totalPenerima.value > 0 ? controller.totalPenerima.value ~/ 10 : 0}',
count: controller.jumlahDiproses.value.toString(),
subtitle: 'Perlu Tindakan',
height: 120,
gradient: LinearGradient(

View File

@ -698,10 +698,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
try {
imageUrl =
await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
print('Berhasil upload bukti penerimaan: $imageUrl');
} catch (e) {
// Jika upload bukti penerimaan gagal, tampilkan pesan dan hentikan proses
print('Error upload bukti penerimaan: $e');
throw Exception('Gagal mengupload bukti penerimaan: $e');
}
@ -711,46 +708,31 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
signatureFile = File('${tempDir.path}/signature.png');
await signatureFile.writeAsBytes(_signatureImage!);
print('Signature file path: ${signatureFile.path}');
print('Signature file exists: ${signatureFile.existsSync()}');
print('Signature file size: ${signatureFile.lengthSync()} bytes');
signatureUrl = await controller.uploadBuktiPenerimaan(
signatureFile.path,
isTandaTangan: true,
);
print('Berhasil upload tanda tangan: $signatureUrl');
} catch (e) {
// Jika upload tanda tangan gagal, tampilkan pesan dan hentikan proses
print('Error upload tanda tangan: $e');
throw Exception('Gagal mengupload tanda tangan: $e');
}
// Konfirmasi penerimaan
try {
print('Melakukan konfirmasi penerimaan untuk ID: ${penerima.id}');
await controller.konfirmasiPenerimaan(
penerima,
buktiPenerimaan: imageUrl,
tandaTangan: signatureUrl,
);
print('Konfirmasi penerimaan berhasil');
} catch (e) {
// Jika konfirmasi penerimaan gagal, tampilkan pesan dan hentikan proses
print('Error konfirmasi penerimaan: $e');
throw Exception('Gagal melakukan konfirmasi penerimaan: $e');
}
// Hapus file sementara sebelum navigasi
try {
if (signatureFile.existsSync()) {
await signatureFile.delete();
}
if (tempDir.existsSync()) {
await tempDir.delete();
}
} catch (e) {
print('Error saat menghapus file sementara: $e');
if (signatureFile.existsSync()) {
await signatureFile.delete();
}
if (tempDir.existsSync()) {
await tempDir.delete();
}
// Tutup semua snackbar yang mungkin masih terbuka

View File

@ -323,144 +323,324 @@ class PengaduanView extends GetView<PengaduanController> {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
}
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
side: BorderSide(
color: statusColor.withOpacity(0.3),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
elevation: 3,
child: InkWell(
onTap: () {
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.warga?['nama'] ?? item.judul ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
// overflow: TextOverflow.ellipsis,
),
// Header dengan warna sesuai status
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
const SizedBox(width: 12),
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,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Icon(
Icons.report_problem,
color: statusColor,
),
const SizedBox(width: 8),
Flexible(
child: Text(
item.warga?['nama'] ?? item.judul ?? '',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: statusColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: statusColor,
width: 1.0,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 14,
color: statusColor,
),
const SizedBox(width: 4),
Text(
item.status ?? '',
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Deskripsi masalah
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.grey.shade200,
width: 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Deskripsi Masalah:',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 6),
Text(
item.deskripsi ?? '',
style: TextStyle(
color: Colors.grey.shade700,
),
),
],
),
),
// Informasi penyaluran bantuan jika ada
if (item.penerimaPenyaluran != null)
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.blue.shade200,
width: 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Penyaluran: ${item.namaPenyaluran ?? "Tidak tersedia"}',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bantuan',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
item.stokBantuan?['nama'] ?? '-',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jumlah',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
'${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
],
),
),
],
),
],
),
),
// Informasi pelapor dan tanggal
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pelapor',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
item.warga?['nama_lengkap'] ?? '-',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'NIK',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
item.warga?['nik'] ?? '-',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
],
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
item.deskripsi ?? '',
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.person,
label: 'Pelapor',
value: item.warga?['nama_lengkap'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.numbers,
label: 'NIK',
value: item.warga?['nik'] ?? '',
),
),
],
),
const SizedBox(height: 12),
if (item.penerimaPenyaluran != null) ...[
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.shopping_bag,
label: 'Jumlah',
value:
'${item.jumlahBantuan} ${item.stokBantuan['satuan']}',
)),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Stok Bantuan',
value: item.stokBantuan['nama'] ?? '',
const SizedBox(height: 12),
// Informasi tanggal
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.calendar_today,
size: 12,
color: Colors.grey.shade700,
),
const SizedBox(width: 4),
Text(
formattedDate,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade800,
),
),
],
),
),
const SizedBox(height: 12),
// Tombol detail
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton.icon(
onPressed: () {
Get.toNamed('/detail-pengaduan',
arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Lihat Detail'),
style: ElevatedButton.styleFrom(
backgroundColor: statusColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.category,
label: 'Nama Penyaluran',
value: item.namaPenyaluran ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Pengaduan',
value: formattedDate,
),
),
],
),
],
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _buildActionButtons(context, item),
),
],
),
@ -505,174 +685,19 @@ class PengaduanView extends GetView<PengaduanController> {
}
List<Widget> _buildActionButtons(BuildContext context, dynamic item) {
final status = item.status?.toUpperCase();
if (status == 'MENUNGGU') {
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),
),
return [
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} 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: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} else {
return [
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
}
}
void _showTindakanDialog(BuildContext context, dynamic item) {
controller.tindakanController.clear();
controller.catatanController.clear();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tindakan Pengaduan'),
content: Form(
key: controller.tindakanFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'),
const SizedBox(height: 16),
TextFormField(
controller: controller.tindakanController,
decoration: const InputDecoration(
labelText: 'Tindakan yang dilakukan',
border: OutlineInputBorder(),
),
maxLines: 3,
validator: controller.validateTindakan,
),
const SizedBox(height: 12),
TextFormField(
controller: controller.catatanController,
decoration: const InputDecoration(
labelText: 'Catatan (opsional)',
border: OutlineInputBorder(),
),
maxLines: 2,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (controller.tindakanFormKey.currentState!.validate()) {
Navigator.pop(context);
controller.tambahTindakanPengaduan(
pengaduanId: item.id!,
tindakan: controller.tindakanController.text,
kategoriTindakan: 'VERIFIKASI_DATA',
statusTindakan: 'PROSES',
catatan: controller.catatanController.text.isEmpty
? null
: controller.catatanController.text,
buktiTindakanPaths: [],
);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showSelesaikanDialog(BuildContext context, dynamic item) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Selesaikan Pengaduan'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'),
const SizedBox(height: 16),
const Text(
'Apakah Anda yakin ingin menyelesaikan pengaduan ini?',
textAlign: TextAlign.center,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
controller.selesaikanPengaduan(item.id!);
},
child: const Text('Selesaikan'),
),
],
),
);
];
}
}

View File

@ -306,236 +306,443 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
color: Colors.grey.withOpacity(0.15),
spreadRadius: 2,
blurRadius: 8,
offset: const Offset(0, 3),
),
],
border: Border.all(
color: statusColor.withOpacity(0.3),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
// Header dengan status
Container(
color: statusColor.withOpacity(0.1),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Expanded(
child: Text(
donaturNama,
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
statusIcon,
size: 16,
color: statusColor,
),
),
if (isDonaturManual)
Tooltip(
message: 'Donatur Manual (Diinput oleh petugas desa)',
child: Container(
margin: const EdgeInsets.only(left: 4),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.shade300),
),
child: const Text(
'Manual',
style: TextStyle(
fontSize: 10,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
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),
const SizedBox(width: 8),
Text(
item.status ?? 'Tidak diketahui',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: isUang ? Icons.monetization_on : Icons.category,
label: 'Kategori Bantuan',
value: kategoriNama,
),
),
Expanded(
child: _buildItemDetail(
context,
icon:
isUang ? Icons.account_balance_wallet : Icons.inventory,
label: 'Jumlah',
value: isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Dibuat',
value: DateTimeHelper.formatDateTime(item.createdAt,
defaultValue: 'Tidak ada tanggal'),
),
),
Expanded(
child: item.status == 'TERVERIFIKASI' &&
item.petugasDesaId != null
? _buildItemDetail(
context,
icon: Icons.person,
label: 'Diverifikasi Oleh',
value:
controller.getPetugasDesaNama(item.petugasDesaId),
)
: const SizedBox(),
),
],
),
// Tampilkan thumbnail foto bantuan jika ada
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.photo_library,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
'Foto Bantuan',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
const SizedBox(width: 4),
Text(
'(${item.fotoBantuan!.length} foto)',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
Text(
DateTimeHelper.formatDate(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
),
),
],
),
),
const SizedBox(height: 12),
if (item.status == 'MENUNGGU')
Row(
mainAxisAlignment: MainAxisAlignment.end,
// Content
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () {
_showVerifikasiDialog(context, item.id ?? '');
},
icon: const Icon(Icons.check, size: 18),
label: const Text('Terima'),
style: TextButton.styleFrom(
foregroundColor: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
// Donatur info
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20,
child: Text(
donaturNama.substring(0, 1).toUpperCase(),
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
donaturNama,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
if (isDonaturManual)
Container(
margin: const EdgeInsets.only(left: 4),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.blue.shade300),
),
child: const Text(
'Manual',
style: TextStyle(
fontSize: 10,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
),
Text(
'Donatur',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Colors.grey,
),
),
],
),
),
],
),
TextButton.icon(
onPressed: () {
_showTolakDialog(context, item.id ?? '');
},
icon: const Icon(Icons.close, size: 18),
label: const Text('Tolak'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 12),
// Informasi bantuan
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUang
? Colors.green.withOpacity(0.1)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isUang
? Icons.monetization_on
: Icons.category,
size: 16,
color: isUang ? Colors.green : Colors.blue,
),
const SizedBox(width: 6),
Text(
'Kategori',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: isUang
? Colors.green
: Colors.blue,
),
),
],
),
const SizedBox(height: 4),
Text(
kategoriNama,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUang
? Colors.amber.withOpacity(0.1)
: Colors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isUang
? Icons.account_balance_wallet
: Icons.inventory,
size: 16,
color: isUang
? Colors.amber.shade800
: Colors.purple,
),
const SizedBox(width: 6),
Text(
'Jumlah',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: isUang
? Colors.amber.shade800
: Colors.purple,
),
),
],
),
const SizedBox(height: 4),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
],
),
TextButton.icon(
onPressed: () {
_showDetailDialog(context, item, donaturNama);
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
// Tampilkan thumbnail foto bantuan jika ada
if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 10),
Row(
children: [
Icon(
Icons.photo_library,
size: 16,
color: Colors.grey.shade700,
),
const SizedBox(width: 6),
Text(
'Foto Bantuan',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${item.fotoBantuan!.length} foto',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
if (item.status == 'TERVERIFIKASI' &&
item.petugasDesaId != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Divider(),
const SizedBox(height: 10),
Row(
children: [
Icon(
Icons.verified_user,
size: 16,
color: Colors.green,
),
const SizedBox(width: 6),
Expanded(
child: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodyMedium,
children: [
TextSpan(
text: 'Diverifikasi oleh ',
style: TextStyle(
color: Colors.grey.shade700),
),
TextSpan(
text: controller.getPetugasDesaNama(
item.petugasDesaId),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
),
],
),
],
),
),
],
),
),
// Footer dengan tombol aksi
if (item.status == 'MENUNGGU')
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade200),
),
),
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
onPressed: () {
_showDetailDialog(context, item, donaturNama);
},
icon: const Icon(Icons.info_outline, size: 16),
label: const Text('Detail'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue.shade300),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {
_showTolakDialog(context, item.id ?? '');
},
icon: const Icon(Icons.close, size: 16),
label: const Text('Tolak'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: BorderSide(color: Colors.red.shade300),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
_showVerifikasiDialog(context, item.id ?? '');
},
icon: const Icon(Icons.check, size: 16),
label: const Text('Terima'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
],
),
)
else
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
_showDetailDialog(context, item, donaturNama);
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade200),
),
],
),
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton.icon(
onPressed: () {
_showDetailDialog(context, item, donaturNama);
},
icon: const Icon(Icons.info_outline, size: 16),
label: const Text('Lihat Detail'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
),
),
],
),
),
],
),

View File

@ -75,8 +75,8 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Jadwal hari ini
JadwalSectionWidget(
controller: controller,
title: 'Hari Ini',
jadwalList: controller.jadwalHariIni,
title: 'Penyaluran Aktif',
jadwalList: controller.jadwalAktif,
status: 'Aktif',
),
@ -158,7 +158,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
context,
icon: Icons.event_available,
title: 'Aktif',
value: '${controller.jadwalHariIni.length}',
value: '${controller.jadwalAktif.length}',
color: Colors.green,
)),
),

View File

@ -343,9 +343,6 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
activeIcon: Icons.warning_amber,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 3,
badge: controller.jumlahDiproses.value > 0
? controller.jumlahDiproses.value.toString()
: null,
onTap: () {
Navigator.pop(context);
controller.changeTab(3);
@ -675,7 +672,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
@ -705,7 +702,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
color: Colors.orange,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(

File diff suppressed because it is too large Load Diff

View File

@ -332,176 +332,283 @@ class StokBantuanView extends GetView<StokBantuanController> {
}
Widget _buildStokBantuanItem(BuildContext context, StokBantuanModel item) {
// Tentukan warna berdasarkan jenis bantuan
Color categoryColor =
item.isUang == true ? Colors.amber.shade700 : AppTheme.primaryColor;
// Cek apakah stok hampir habis (kurang dari 10)
bool isLowStock = !item.isUang! && item.totalStok! < 10;
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
color: Colors.grey.withAlpha(30),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.nama ?? 'Tanpa Nama',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan gradient berdasarkan jenis bantuan
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
categoryColor.withOpacity(0.8),
categoryColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
Expanded(
child: Text(
item.nama ?? 'Tanpa Nama',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
item.isUang == true
? Icons.monetization_on
: Icons.inventory_2_outlined,
size: 14,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
const SizedBox(width: 4),
Text(
item.kategoriBantuan != null
? (item.kategoriBantuan!['nama'] ??
'Tidak Ada Kategori')
: 'Tidak Ada Kategori',
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
),
// Body content
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Deskripsi
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
item.deskripsi!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Detail stok/dana dalam card
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isLowStock
? Colors.red.shade50
: (item.isUang == true
? Colors.amber.shade50
: Colors.blue.shade50),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isLowStock
? Colors.red.shade200
: (item.isUang == true
? Colors.amber.shade200
: Colors.blue.shade200),
shape: BoxShape.circle,
),
child: Icon(
item.isUang == true
? Icons.monetization_on
: (isLowStock
? Icons.warning_amber_rounded
: Icons.inventory),
size: 20,
color: isLowStock
? Colors.red.shade800
: (item.isUang == true
? Colors.amber.shade800
: Colors.blue.shade800),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.isUang == true
? 'Total Dana'
: (isLowStock
? 'Stok Hampir Habis!'
: 'Total Stok'),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
color: isLowStock
? Colors.red.shade800
: (item.isUang == true
? Colors.amber.shade800
: Colors.blue.shade800),
),
),
Text(
item.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.bold,
color: isLowStock
? Colors.red.shade900
: (item.isUang == true
? Colors.amber.shade900
: Colors.blue.shade900),
),
),
],
),
),
],
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.isUang == true)
const Icon(
Icons.monetization_on,
size: 16,
color: AppTheme.primaryColor,
),
if (item.isUang == true) const SizedBox(width: 4),
Text(
item.kategoriBantuan != null
? (item.kategoriBantuan!['nama'] ??
'Tidak Ada Kategori')
: 'Tidak Ada Kategori',
const SizedBox(height: 16),
// Additional details
Row(
children: [
Icon(
Icons.access_time,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
item.updatedAt != null
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}'
: 'Tidak ada data pembaruan',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
),
],
),
const SizedBox(height: 12),
// Tombol Aksi
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade200),
),
),
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton.icon(
onPressed: () {
// Tampilkan dialog edit stok bantuan
_showEditStokDialog(context, item);
},
icon: const Icon(Icons.edit_outlined, size: 16),
label: const Text('Edit'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue.shade300),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
_showDeleteConfirmation(context, item);
},
icon: const Icon(Icons.delete_outline, size: 16),
label: const Text('Hapus'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: BorderSide(color: Colors.red.shade300),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
],
),
),
],
),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
item.deskripsi!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: item.isUang == true
? Icons.monetization_on
: Icons.inventory,
label: item.isUang == true ? 'Total Dana' : 'Total Stok',
value: item.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.access_time,
label: 'Terakhir Diperbarui',
value: item.updatedAt != null
? '${item.updatedAt!.day}/${item.updatedAt!.month}/${item.updatedAt!.year} ${item.updatedAt!.hour}:${item.updatedAt!.minute}'
: 'Tidak ada data',
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Tampilkan dialog edit stok bantuan
_showEditStokDialog(context, item);
},
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
_showDeleteConfirmation(context, item);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
],
),
);
}

View File

@ -129,7 +129,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
const SizedBox(height: 16),
// Skema Bantuan
Text(
@ -175,6 +175,18 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
}
await loadPengajuanKelayakan(value);
// Periksa apakah ada penerima
if (jumlahPenerima.value == 0) {
Get.snackbar(
'Perhatian',
'Skema bantuan ini tidak memiliki penerima yang terverifikasi!',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
}
}
},
validator: (value) {
@ -184,6 +196,37 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
return null;
},
)),
// const SizedBox(height: 16),
// Pesan pemberitahuan jika tidak ada penerima
Obx(() => jumlahPenerima.value == 0 &&
selectedSkemaBantuanId.value != null
? Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.warning_amber_rounded,
color: Colors.red.shade700),
const SizedBox(width: 12),
Expanded(
child: Text(
'Skema bantuan ini tidak memiliki penerima yang terverifikasi. Tambahkan penerima terlebih dahulu.',
style: TextStyle(
color: Colors.red.shade700,
fontWeight: FontWeight.w500,
),
),
),
],
),
)
: const SizedBox()),
const SizedBox(height: 16),
// Jumlah Penerima (Otomatis)
Row(
@ -755,67 +798,78 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
// Tombol Submit
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
// Periksa kecukupan stok
if (!isStokCukup.value) {
Get.snackbar(
'Stok Tidak Cukup',
'Stok bantuan tidak mencukupi untuk penyaluran ini. Silakan tambah stok terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
return;
}
child: Obx(() => ElevatedButton(
onPressed: jumlahPenerima.value > 0
? () {
if (formKey.currentState!.validate()) {
// Periksa kecukupan stok
if (!isStokCukup.value) {
Get.snackbar(
'Stok Tidak Cukup',
'Stok bantuan tidak mencukupi untuk penyaluran ini. Silakan tambah stok terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
return;
}
// Gabungkan tanggal dan waktu mulai
DateTime? tanggalWaktuMulai;
if (selectedDate.value != null &&
selectedWaktuMulai.value != null) {
tanggalWaktuMulai = DateTime(
selectedDate.value!.year,
selectedDate.value!.month,
selectedDate.value!.day,
selectedWaktuMulai.value!.hour,
selectedWaktuMulai.value!.minute,
).toLocal();
}
// Gabungkan tanggal dan waktu mulai
DateTime? tanggalWaktuMulai;
if (selectedDate.value != null &&
selectedWaktuMulai.value != null) {
tanggalWaktuMulai = DateTime(
selectedDate.value!.year,
selectedDate.value!.month,
selectedDate.value!.day,
selectedWaktuMulai.value!.hour,
selectedWaktuMulai.value!.minute,
).toLocal();
}
// Panggil fungsi untuk menambahkan penyaluran
controller.tambahPenyaluran(
nama: namaController.text,
deskripsi: deskripsiController.text,
skemaId: selectedSkemaBantuanId.value!,
lokasiPenyaluranId: selectedLokasiPenyaluranId.value!,
jumlahPenerima: jumlahPenerima.value,
tanggalPenyaluran: tanggalWaktuMulai,
kategoriBantuanId:
selectedSkemaBantuan.value!.kategoriBantuanId!,
jumlahDiterimaPerOrang: jumlahDiterimaPerOrang.value,
stokBantuanId:
selectedSkemaBantuan.value!.stokBantuanId!,
totalStokDibutuhkan: totalStokDibutuhkan.value);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Simpan Penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
// Panggil fungsi untuk menambahkan penyaluran
controller.tambahPenyaluran(
nama: namaController.text,
deskripsi: deskripsiController.text,
skemaId: selectedSkemaBantuanId.value!,
lokasiPenyaluranId:
selectedLokasiPenyaluranId.value!,
jumlahPenerima: jumlahPenerima.value,
tanggalPenyaluran: tanggalWaktuMulai,
kategoriBantuanId: selectedSkemaBantuan
.value!.kategoriBantuanId!,
jumlahDiterimaPerOrang:
jumlahDiterimaPerOrang.value,
stokBantuanId: selectedSkemaBantuan
.value!.stokBantuanId!,
totalStokDibutuhkan:
totalStokDibutuhkan.value);
//get back and refresh page
Get.back();
controller.refreshData();
}
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
disabledBackgroundColor: Colors.grey.shade300,
disabledForegroundColor: Colors.grey.shade600,
),
child: const Text(
'Simpan Penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
)),
),
],
),