Perbarui status dan model penyaluran untuk konsistensi dan fungsionalitas

- Ganti enum StatusKelayakan dari 'pending', 'disetujui', 'ditolak' menjadi 'MENUNGGU', 'TERVERIFIKASI', 'DITOLAK'
- Ubah referensi 'jadwalSelesai' menjadi 'jadwalTerlaksana' di beberapa komponen
- Perbarui ikon dan warna status di JadwalSectionWidget
- Tambahkan logika baru untuk menangani status 'BATALTERLAKSANA' dan 'TERLAKSANA' di JadwalPenyaluranController
- Modifikasi tampilan untuk menampilkan 'Terlaksana' dan 'Jumlah Penerima' secara otomatis di TambahPenyaluranView
- Tambahkan fungsi untuk memuat data skema bantuan dan pengajuan kelayakan yang disetujui
This commit is contained in:
Khafidh Fuadi
2025-03-14 23:17:02 +07:00
parent ecc1ccac59
commit 0e757c0b94
7 changed files with 284 additions and 93 deletions

View File

@ -1,6 +1,6 @@
import 'dart:convert';
enum StatusKelayakan { pending, disetujui, ditolak }
enum StatusKelayakan { MENUNGGU, TERVERIFIKASI, DITOLAK }
class PengajuanKelayakanBantuanModel {
final String? id;

View File

@ -245,7 +245,7 @@ class CalendarViewWidget extends StatelessWidget {
List<PenyaluranBantuanModel> allJadwal = [
...controller.jadwalHariIni,
...controller.jadwalMendatang,
...controller.jadwalSelesai,
...controller.jadwalTerlaksana,
];
DateTime now = DateTime.now();

View File

@ -92,8 +92,10 @@ class JadwalSectionWidget extends StatelessWidget {
return Icons.event_note;
case 'Terjadwal':
return Icons.pending_actions;
case 'Selesai':
case 'Terlaksana':
return Icons.event_available;
case 'Tidak Terlaksana':
return Icons.event_busy;
default:
return Icons.event_note;
}
@ -105,8 +107,9 @@ class JadwalSectionWidget extends StatelessWidget {
return Icons.calendar_today;
case 'Terjadwal':
return Icons.schedule;
case 'Selesai':
case 'Terlaksana':
return Icons.task_alt;
default:
return Icons.event_note;
}
@ -118,7 +121,7 @@ class JadwalSectionWidget extends StatelessWidget {
return Colors.green;
case 'Terjadwal':
return Colors.blue;
case 'Selesai':
case 'Terlaksana':
return Colors.grey;
default:
return Colors.orange;
@ -127,16 +130,18 @@ class JadwalSectionWidget extends StatelessWidget {
String _getStatusText(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, tampilkan sebagai "Aktif"
if (jadwal.status == 'BERLANGSUNG') {
if (jadwal.status == 'AKTIF') {
return 'Aktif';
}
// Jika status jadwal adalah DIJADWALKAN, tampilkan sebagai "Terjadwal"
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
else if (jadwal.status == 'DIJADWALKAN') {
return 'Terjadwal';
}
// Jika status jadwal adalah SELESAI, tampilkan sebagai "Selesai"
else if (jadwal.status == 'SELESAI') {
return 'Selesai';
// Jika status jadwal adalah terlaksana, tampilkan sebagai "Terlaksana"
else if (jadwal.status == 'TERLAKSANA') {
return 'Terlaksana';
} else if (jadwal.status == 'BATALTERLAKSANA') {
return 'Batal Terlaksana';
}
// Default status
return status;
@ -144,16 +149,16 @@ class JadwalSectionWidget extends StatelessWidget {
Color _getStatusColorByJadwal(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, gunakan warna hijau
if (jadwal.status == 'BERLANGSUNG') {
if (jadwal.status == 'AKTIF') {
return Colors.green;
}
// Jika status jadwal adalah DIJADWALKAN, gunakan warna biru
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
else if (jadwal.status == 'DIJADWALKAN') {
return Colors.blue;
}
// Jika status jadwal adalah SELESAI, gunakan warna abu-abu
else if (jadwal.status == 'SELESAI') {
} else if (jadwal.status == 'TERLAKSANA') {
return Colors.grey;
} else if (jadwal.status == 'BATALTERLAKSANA') {
return Colors.red;
}
// Default warna
return _getStatusColor();
@ -165,8 +170,8 @@ class JadwalSectionWidget extends StatelessWidget {
return controller.jadwalHariIni.toList();
case 'Mendatang':
return controller.jadwalMendatang.toList();
case 'Selesai':
return controller.jadwalSelesai.toList();
case 'Terlaksana':
return controller.jadwalTerlaksana.toList();
default:
return jadwalList;
}

View File

@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/lokasi_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/skema_bantuan_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/utils/date_time_helper.dart';
@ -13,6 +14,8 @@ class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
SupabaseService get supabaseService => _supabaseService;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
@ -23,7 +26,7 @@ class JadwalPenyaluranController extends GetxController {
<PenyaluranBantuanModel>[].obs;
final RxList<PenyaluranBantuanModel> jadwalMendatang =
<PenyaluranBantuanModel>[].obs;
final RxList<PenyaluranBantuanModel> jadwalSelesai =
final RxList<PenyaluranBantuanModel> jadwalTerlaksana =
<PenyaluranBantuanModel>[].obs;
// Data untuk permintaan penjadwalan
@ -36,6 +39,8 @@ class JadwalPenyaluranController extends GetxController {
<String, LokasiPenyaluranModel>{}.obs;
final RxMap<String, KategoriBantuanModel> kategoriBantuanCache =
<String, KategoriBantuanModel>{}.obs;
final RxMap<String, SkemaBantuanModel> skemaBantuanCache =
<String, SkemaBantuanModel>{}.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
@ -49,6 +54,7 @@ class JadwalPenyaluranController extends GetxController {
loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData();
loadKategoriBantuanData();
loadSkemaBantuanData();
// Jalankan timer untuk memeriksa jadwal secara berkala
_startJadwalCheckTimer();
@ -89,8 +95,9 @@ class JadwalPenyaluranController extends GetxController {
// Periksa jadwal mendatang yang tanggalnya hari ini
List<PenyaluranBantuanModel> jadwalToUpdate = [];
List<PenyaluranBantuanModel> jadwalTerlewat = [];
for (var jadwal in jadwalMendatang) {
for (var jadwal in jadwalHariIni) {
if (jadwal.tanggalPenyaluran != null) {
// Konversi tanggal jadwal ke timezone lokal
final jadwalDateTime =
@ -103,24 +110,29 @@ class JadwalPenyaluranController extends GetxController {
// Jika tanggal jadwal adalah hari ini
if (isSameDay(jadwalDate, today)) {
jadwalToUpdate.add(jadwal);
// Jika waktu jadwal sudah tiba atau lewat
if (now.isAfter(jadwalDateTime) ||
now.isAtSameMomentAs(jadwalDateTime)) {
// Ubah status menjadi BERLANGSUNG (aktif)
if (jadwal.status == 'DIJADWALKAN') {
// Jika status masih DIJADWALKAN, ubah menjadi BATALTERLAKSANA
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BERLANGSUNG');
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal);
} else if (jadwal.status == 'AKTIF') {
// Jika status BERLANGSUNG, tambahkan ke daftar update
jadwalToUpdate.add(jadwal);
}
}
}
}
}
// Refresh data setelah pembaruan
if (jadwalToUpdate.isNotEmpty) {
if (jadwalToUpdate.isNotEmpty || jadwalTerlewat.isNotEmpty) {
await loadJadwalData();
// Tampilkan notifikasi jika ada jadwal yang dipindahkan
if (jadwalToUpdate.isNotEmpty) {
Get.snackbar(
'Jadwal Diperbarui',
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
@ -130,6 +142,19 @@ class JadwalPenyaluranController extends GetxController {
duration: const Duration(seconds: 3),
);
}
// Tampilkan notifikasi jika ada jadwal yang terlewat
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
} catch (e) {
print('Error checking and updating jadwal status: $e');
}
@ -162,9 +187,9 @@ class JadwalPenyaluranController extends GetxController {
}
// Mengambil data jadwal selesai
final jadwalSelesaiData = await _supabaseService.getJadwalSelesai();
if (jadwalSelesaiData != null) {
jadwalSelesai.value = jadwalSelesaiData
final jadwalTerlaksanaData = await _supabaseService.getJadwalTerlaksana();
if (jadwalTerlaksanaData != null) {
jadwalTerlaksana.value = jadwalTerlaksanaData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
@ -219,6 +244,22 @@ class JadwalPenyaluranController extends GetxController {
}
}
Future<void> loadSkemaBantuanData() async {
try {
final skemaData = await _supabaseService.getAllSkemaBantuan();
if (skemaData != null) {
for (var skema in skemaData) {
final skemaModel = SkemaBantuanModel.fromJson(skema);
if (skemaModel.id != null) {
skemaBantuanCache[skemaModel.id!] = skemaModel;
}
}
}
} catch (e) {
print('Error loading skema bantuan data: $e');
}
}
// Mendapatkan nama lokasi penyaluran berdasarkan ID
String getLokasiPenyaluranName(String? lokasiId) {
if (lokasiId == null) return 'Lokasi tidak diketahui';
@ -333,7 +374,7 @@ class JadwalPenyaluranController extends GetxController {
Future<void> tambahPenyaluran({
required String nama,
required String deskripsi,
required String kategoriBantuanId,
required String skemaId,
required String lokasiPenyaluranId,
required int jumlahPenerima,
required DateTime? tanggalPenyaluran,
@ -349,7 +390,7 @@ class JadwalPenyaluranController extends GetxController {
final penyaluran = {
'nama': nama,
'deskripsi': deskripsi,
'kategori_bantuan_id': kategoriBantuanId,
'skema_id': skemaId,
'lokasi_penyaluran_id': lokasiPenyaluranId,
'petugas_id': user!.id,
'jumlah_penerima': jumlahPenerima,
@ -357,8 +398,30 @@ class JadwalPenyaluranController extends GetxController {
'status': 'DIJADWALKAN', // Status awal adalah terjadwal
};
// Simpan ke database
await _supabaseService.tambahPenyaluran(penyaluran);
// Simpan ke database dan dapatkan ID penyaluran
final response = await _supabaseService.tambahPenyaluran(penyaluran);
final penyaluranId = response['id'];
// Ambil data pengajuan kelayakan bantuan yang disetujui
final pengajuanData = await _supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*')
.eq('skema_bantuan_id', skemaId)
.eq('status', 'TERVERIFIKASI');
// Buat data penerima penyaluran untuk setiap pengajuan yang disetujui
for (var pengajuan in pengajuanData) {
final penerimaPenyaluran = {
'penyaluran_bantuan_id': penyaluranId,
'warga_id': pengajuan['warga_id'],
'stok_bantuan_id': skemaBantuanCache[skemaId]?.stokBantuanId,
'status_penerimaan': 'MENUNGGU',
};
await _supabaseService.client
.from('penerima_penyaluran')
.insert(penerimaPenyaluran);
}
// Refresh data
await loadJadwalData();

View File

@ -101,9 +101,9 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Jadwal selesai
JadwalSectionWidget(
controller: controller,
title: 'Selesai',
jadwalList: controller.jadwalSelesai,
status: 'Selesai',
title: 'Terlaksana',
jadwalList: controller.jadwalTerlaksana,
status: 'Terlaksana',
),
],
);
@ -190,9 +190,9 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
Expanded(
child: Obx(() => _buildSummaryItem(
context,
icon: Icons.event_busy,
title: 'Selesai',
value: '${controller.jadwalSelesai.length}',
icon: Icons.event_note,
title: 'Terlaksana',
value: '${controller.jadwalTerlaksana.length}',
color: Colors.grey,
)),
),

View File

@ -3,6 +3,8 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart';
class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
const TambahPenyaluranView({super.key});
@ -23,21 +25,38 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
final formKey = GlobalKey<FormState>();
final TextEditingController namaController = TextEditingController();
final TextEditingController deskripsiController = TextEditingController();
final TextEditingController jumlahPenerimaController =
TextEditingController();
final TextEditingController tanggalPenyaluranController =
TextEditingController();
final TextEditingController waktuPenyaluranController =
TextEditingController();
// Variabel untuk menyimpan nilai yang dipilih
final Rx<String?> selectedKategoriBantuanId = Rx<String?>(null);
final Rx<String?> selectedSkemaBantuanId = Rx<String?>(null);
final Rx<String?> selectedLokasiPenyaluranId = Rx<String?>(null);
final Rx<SkemaBantuanModel?> selectedSkemaBantuan =
Rx<SkemaBantuanModel?>(null);
final RxInt jumlahPenerima = 0.obs;
// Tanggal dan waktu penyaluran
final Rx<DateTime?> selectedDate = Rx<DateTime?>(null);
final Rx<TimeOfDay?> selectedTime = Rx<TimeOfDay?>(null);
// Fungsi untuk memuat data pengajuan kelayakan bantuan
Future<void> loadPengajuanKelayakan(String skemaId) async {
try {
final pengajuanData = await controller.supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*')
.eq('skema_bantuan_id', skemaId)
.eq('status', 'TERVERIFIKASI');
print('pengajuan $pengajuanData');
jumlahPenerima.value = pengajuanData.length;
} catch (e) {
print('Error loading pengajuan kelayakan: $e');
}
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
@ -82,9 +101,9 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
),
const SizedBox(height: 16),
// Kategori Bantuan
// Skema Bantuan
Text(
'Kategori Bantuan',
'Skema Bantuan',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
@ -98,20 +117,25 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
vertical: 8,
),
),
hint: const Text('Pilih kategori bantuan'),
value: selectedKategoriBantuanId.value,
items: controller.kategoriBantuanCache.entries
hint: const Text('Pilih skema bantuan'),
value: selectedSkemaBantuanId.value,
items: controller.skemaBantuanCache.entries
.map((entry) => DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value.nama ?? 'Tidak ada nama'),
))
.toList(),
onChanged: (value) {
selectedKategoriBantuanId.value = value;
onChanged: (value) async {
selectedSkemaBantuanId.value = value;
if (value != null) {
selectedSkemaBantuan.value =
controller.skemaBantuanCache[value];
await loadPengajuanKelayakan(value);
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Kategori bantuan harus dipilih';
return 'Skema bantuan harus dipilih';
}
return null;
},
@ -154,17 +178,18 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
)),
const SizedBox(height: 16),
// Jumlah Penerima
// Jumlah Penerima (Otomatis)
Text(
'Jumlah Penerima',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: jumlahPenerimaController,
keyboardType: TextInputType.number,
Obx(() => TextFormField(
readOnly: true,
controller: TextEditingController(
text: jumlahPenerima.value.toString()),
decoration: InputDecoration(
hintText: 'Masukkan jumlah penerima',
hintText: 'Jumlah penerima akan diambil otomatis',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
@ -173,19 +198,97 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah penerima tidak boleh kosong';
}
if (int.tryParse(value) == null) {
return 'Jumlah penerima harus berupa angka';
}
if (int.parse(value) <= 0) {
return 'Jumlah penerima harus lebih dari 0';
}
return null;
},
)),
const SizedBox(height: 8),
Obx(() => jumlahPenerima.value > 0
? TextButton.icon(
onPressed: () async {
final pengajuanData = await controller
.supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*, warga:warga_id(*)')
.eq('skema_bantuan_id',
selectedSkemaBantuanId.value ?? '')
.eq('status', 'TERVERIFIKASI');
if (pengajuanData != null) {
Get.dialog(
Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height:
MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Daftar Penerima Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
columnSpacing: 20,
horizontalMargin: 20,
columns: const [
DataColumn(label: Text('No')),
DataColumn(label: Text('Nama')),
DataColumn(label: Text('NIK')),
DataColumn(label: Text('Alamat')),
],
rows: pengajuanData
.asMap()
.entries
.map((entry) {
final warga =
entry.value['warga'];
return DataRow(
cells: [
DataCell(
Text('${entry.key + 1}')),
DataCell(Text(
warga['nama_lengkap'] ??
'-')),
DataCell(Text(
warga['nik'] ?? '-')),
DataCell(Text(
warga['alamat'] ?? '-')),
],
);
}).toList(),
),
),
),
),
],
),
),
),
);
}
},
icon: const Icon(Icons.people),
label: const Text('Lihat Daftar Penerima'),
)
: const SizedBox.shrink()),
const SizedBox(height: 16),
// Tanggal Penyaluran
@ -321,10 +424,9 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
controller.tambahPenyaluran(
nama: namaController.text,
deskripsi: deskripsiController.text,
kategoriBantuanId: selectedKategoriBantuanId.value!,
skemaId: selectedSkemaBantuanId.value!,
lokasiPenyaluranId: selectedLokasiPenyaluranId.value!,
jumlahPenerima:
int.parse(jumlahPenerimaController.text),
jumlahPenerima: jumlahPenerima.value,
tanggalPenyaluran: tanggalWaktuPenyaluran,
);
}

View File

@ -242,7 +242,7 @@ class SupabaseService extends GetxService {
.select('*')
.gte('tanggal_penyaluran', todayUtc)
.lt('tanggal_penyaluran', tomorrowUtc)
.inFilter('status', ['DISETUJUI', 'BERLANGSUNG', 'DIJADWALKAN']);
.inFilter('status', ['AKTIF', 'DIJADWALKAN']);
return response;
} catch (e) {
@ -267,7 +267,7 @@ class SupabaseService extends GetxService {
.select('*')
.gte('tanggal_penyaluran', tomorrowUtc)
.lt('tanggal_penyaluran', weekUtc)
.inFilter('status', ['DISETUJUI', 'DIJADWALKAN']);
.inFilter('status', ['DIJADWALKAN']);
return response;
} catch (e) {
@ -276,12 +276,12 @@ class SupabaseService extends GetxService {
}
}
Future<List<Map<String, dynamic>>?> getJadwalSelesai() async {
Future<List<Map<String, dynamic>>?> getJadwalTerlaksana() async {
try {
final response = await client
.from('penyaluran_bantuan')
.select('*')
.eq('status', 'SELESAI')
.eq('status', 'TERLAKSANA')
.order('tanggal_penyaluran', ascending: false)
.limit(10);
@ -1124,12 +1124,33 @@ class SupabaseService extends GetxService {
}
// Fungsi untuk menambahkan penyaluran baru
Future<void> tambahPenyaluran(Map<String, dynamic> penyaluran) async {
Future<Map<String, dynamic>> tambahPenyaluran(
Map<String, dynamic> penyaluran) async {
try {
await client.from('penyaluran_bantuan').insert(penyaluran);
final response = await client
.from('penyaluran_bantuan')
.insert(penyaluran)
.select()
.single();
return response;
} catch (e) {
print('Error menambahkan penyaluran: $e');
throw e.toString();
}
}
Future<List<Map<String, dynamic>>?> getAllSkemaBantuan() async {
try {
final response = await client
.from('xx02_skema_bantuan')
.select('*')
.order('created_at', ascending: false);
return response;
} catch (e) {
print('Error getting all skema bantuan: $e');
return null;
}
}
}