Perbarui model dan tampilan untuk mendukung fitur tanda tangan dan pembatalan penyaluran. Modifikasi PenerimaPenyaluranModel untuk menambahkan properti tandaTangan. Ubah PenyaluranBantuanModel dengan mengganti alasanPenolakan menjadi alasanPembatalan dan menambahkan tanggalPembatalan serta tanggalSelesai. Perbarui DetailPenyaluranController untuk menangani data penyaluran dan penerima dengan lebih baik. Tambahkan logika baru di DetailPenyaluranPage untuk menampilkan informasi pembatalan dan tanda tangan. Perbarui tampilan KonfirmasiPenerimaPage untuk menyertakan fitur tanda tangan saat konfirmasi penerimaan.

This commit is contained in:
Khafidh Fuadi
2025-03-16 08:42:51 +07:00
parent da06611c3a
commit 49b60f3195
155 changed files with 21110 additions and 769 deletions

View File

@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class JadwalSectionWidget extends StatelessWidget {
final JadwalPenyaluranController controller;
@ -116,18 +117,18 @@ class JadwalSectionWidget extends StatelessWidget {
Color _getStatusColor() {
switch (status) {
case 'Aktif':
return Colors.green;
return AppTheme.scheduledColor;
case 'Terjadwal':
return Colors.blue;
return AppTheme.processedColor;
case 'Terlaksana':
return Colors.grey;
return AppTheme.completedColor;
default:
return Colors.orange;
return AppTheme.infoColor;
}
}
String _getStatusText(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, tampilkan sebagai "Aktif"
// Jika status jadwal adalah AKTIF, tampilkan sebagai "Aktif"
if (jadwal.status == 'AKTIF') {
return 'Aktif';
}
@ -135,7 +136,7 @@ class JadwalSectionWidget extends StatelessWidget {
else if (jadwal.status == 'DIJADWALKAN') {
return 'Terjadwal';
}
// Jika status jadwal adalah terlaksana, tampilkan sebagai "Terlaksana"
// Jika status jadwal adalah TERLAKSANA, tampilkan sebagai "Terlaksana"
else if (jadwal.status == 'TERLAKSANA') {
return 'Terlaksana';
} else if (jadwal.status == 'BATALTERLAKSANA') {
@ -146,17 +147,17 @@ class JadwalSectionWidget extends StatelessWidget {
}
Color _getStatusColorByJadwal(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, gunakan warna hijau
// Jika status jadwal adalah AKTIF, gunakan warna hijau
if (jadwal.status == 'AKTIF') {
return Colors.green;
return AppTheme.scheduledColor;
}
// Jika status jadwal adalah DIJADWALKAN, gunakan warna biru
else if (jadwal.status == 'DIJADWALKAN') {
return Colors.blue;
return AppTheme.processedColor;
} else if (jadwal.status == 'TERLAKSANA') {
return Colors.grey;
return AppTheme.completedColor;
} else if (jadwal.status == 'BATALTERLAKSANA') {
return Colors.red;
return AppTheme.errorColor;
}
// Default warna
return _getStatusColor();

View File

@ -1,212 +1,212 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
// import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
// import 'package:penyaluran_app/app/routes/app_pages.dart';
// import 'package:penyaluran_app/app/theme/app_theme.dart';
// import 'package:penyaluran_app/app/utils/date_formatter.dart';
class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
final JadwalPenyaluranController controller;
// class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
// final JadwalPenyaluranController controller;
const PermintaanPenjadwalanSummaryWidget({
super.key,
required this.controller,
});
// const PermintaanPenjadwalanSummaryWidget({
// super.key,
// required this.controller,
// });
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
// @override
// Widget build(BuildContext context) {
// final textTheme = Theme.of(context).textTheme;
return Obx(() {
final jumlahPermintaan = controller.jumlahPermintaanPenjadwalan.value;
final permintaanList = controller.permintaanPenjadwalan;
// return Obx(() {
// final jumlahPermintaan = controller.jumlahPermintaanPenjadwalan.value;
// final permintaanList = controller.permintaanPenjadwalan;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(30),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: jumlahPermintaan > 0
? Colors.orange.withAlpha(50)
: Colors.grey.withAlpha(30),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Permintaan Penjadwalan',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: jumlahPermintaan > 0
? Colors.red.withAlpha(26)
: Colors.grey.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$jumlahPermintaan',
style: textTheme.bodySmall?.copyWith(
color: jumlahPermintaan > 0 ? Colors.red : Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
if (jumlahPermintaan == 0)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Icon(
Icons.event_note,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Tidak ada permintaan penjadwalan',
style: textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
)
else
Column(
children: [
...permintaanList.take(1).map((permintaan) =>
_buildPermintaanPreview(textTheme, permintaan)),
if (jumlahPermintaan > 1)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'+ ${jumlahPermintaan - 1} permintaan lainnya',
style: textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.permintaanPenjadwalan),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.visibility),
label: const Text('Lihat Semua Permintaan'),
),
),
],
),
);
});
}
// return Container(
// width: double.infinity,
// padding: const EdgeInsets.all(16),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withAlpha(30),
// blurRadius: 10,
// offset: const Offset(0, 4),
// ),
// ],
// border: Border.all(
// color: jumlahPermintaan > 0
// ? Colors.orange.withAlpha(50)
// : Colors.grey.withAlpha(30),
// width: 1,
// ),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// 'Permintaan Penjadwalan',
// style: textTheme.titleMedium?.copyWith(
// fontWeight: FontWeight.bold,
// ),
// ),
// Container(
// padding:
// const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// decoration: BoxDecoration(
// color: jumlahPermintaan > 0
// ? Colors.red.withAlpha(26)
// : Colors.grey.withAlpha(26),
// borderRadius: BorderRadius.circular(12),
// ),
// child: Text(
// '$jumlahPermintaan',
// style: textTheme.bodySmall?.copyWith(
// color: jumlahPermintaan > 0 ? Colors.red : Colors.grey,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// ],
// ),
// const SizedBox(height: 12),
// if (jumlahPermintaan == 0)
// Center(
// child: Padding(
// padding: const EdgeInsets.symmetric(vertical: 16),
// child: Column(
// children: [
// Icon(
// Icons.event_note,
// size: 48,
// color: Colors.grey.shade400,
// ),
// const SizedBox(height: 8),
// Text(
// 'Tidak ada permintaan penjadwalan',
// style: textTheme.bodyMedium?.copyWith(
// color: Colors.grey.shade600,
// ),
// ),
// ],
// ),
// ),
// )
// else
// Column(
// children: [
// ...permintaanList.take(1).map((permintaan) =>
// _buildPermintaanPreview(textTheme, permintaan)),
// if (jumlahPermintaan > 1)
// Padding(
// padding: const EdgeInsets.only(top: 8),
// child: Text(
// '+ ${jumlahPermintaan - 1} permintaan lainnya',
// style: textTheme.bodySmall?.copyWith(
// color: Colors.grey.shade600,
// ),
// ),
// ),
// ],
// ),
// const SizedBox(height: 16),
// SizedBox(
// width: double.infinity,
// child: ElevatedButton.icon(
// onPressed: () => Get.toNamed(Routes.permintaanPenjadwalan),
// style: ElevatedButton.styleFrom(
// backgroundColor: AppTheme.primaryColor,
// foregroundColor: Colors.white,
// padding: const EdgeInsets.symmetric(vertical: 12),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// icon: const Icon(Icons.visibility),
// label: const Text('Lihat Semua Permintaan'),
// ),
// ),
// ],
// ),
// );
// });
// }
Widget _buildPermintaanPreview(
TextTheme textTheme, PenyaluranBantuanModel permintaan) {
// Format tanggal
String formattedDate = permintaan.tanggalPermintaan != null
? DateFormat('dd MMMM yyyy').format(permintaan.tanggalPermintaan!)
: 'Belum ditentukan';
// Widget _buildPermintaanPreview(
// TextTheme textTheme, PenyaluranBantuanModel permintaan) {
// // Format tanggal
// String formattedDate = permintaan.tanggalPermintaan != null
// ? DateFormatter.formatDate(permintaan.tanggalPermintaan!)
// : 'Belum ditentukan';
// Dapatkan nama kategori
String kategoriName =
controller.getKategoriBantuanName(permintaan.kategoriBantuanId);
// // Dapatkan nama kategori
// String kategoriName =
// controller.getKategoriBantuanName(permintaan.kategoriBantuanId);
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.withAlpha(15),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
permintaan.nama ?? 'Tanpa Nama',
style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Menunggu',
style: textTheme.bodySmall?.copyWith(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
],
),
const SizedBox(height: 4),
if (permintaan.deskripsi != null && permintaan.deskripsi!.isNotEmpty)
Text(
permintaan.deskripsi!,
style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Text(
'Kategori: $kategoriName',
style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
Text(
'Tanggal: $formattedDate',
style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
// return Container(
// width: double.infinity,
// margin: const EdgeInsets.only(bottom: 8),
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// color: Colors.grey.withAlpha(15),
// borderRadius: BorderRadius.circular(8),
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Text(
// permintaan.nama ?? 'Tanpa Nama',
// style: textTheme.titleSmall?.copyWith(
// fontWeight: FontWeight.bold,
// ),
// overflow: TextOverflow.ellipsis,
// ),
// ),
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
// decoration: BoxDecoration(
// color: Colors.orange.withAlpha(26),
// borderRadius: BorderRadius.circular(8),
// ),
// child: Text(
// 'Menunggu',
// style: textTheme.bodySmall?.copyWith(
// color: Colors.orange,
// fontWeight: FontWeight.bold,
// fontSize: 10,
// ),
// ),
// ),
// ],
// ),
// const SizedBox(height: 4),
// if (permintaan.deskripsi != null && permintaan.deskripsi!.isNotEmpty)
// Text(
// permintaan.deskripsi!,
// style: textTheme.bodySmall,
// overflow: TextOverflow.ellipsis,
// maxLines: 1,
// ),
// Text(
// 'Kategori: $kategoriName',
// style: textTheme.bodySmall,
// overflow: TextOverflow.ellipsis,
// ),
// Text(
// 'Tanggal: $formattedDate',
// style: textTheme.bodySmall,
// overflow: TextOverflow.ellipsis,
// ),
// ],
// ),
// );
// }
// }

View File

@ -381,7 +381,6 @@ class JadwalPenyaluranController extends GetxController {
required String lokasiPenyaluranId,
required int jumlahPenerima,
required DateTime? tanggalPenyaluran,
DateTime? tanggalWaktuSelesai,
}) async {
isLoading.value = true;
try {
@ -399,7 +398,6 @@ class JadwalPenyaluranController extends GetxController {
'petugas_id': user!.id,
'jumlah_penerima': jumlahPenerima,
'tanggal_penyaluran': tanggalPenyaluran?.toUtc().toIso8601String(),
'tanggal_waktu_selesai': tanggalWaktuSelesai?.toUtc().toIso8601String(),
'status': 'DIJADWALKAN', // Status awal adalah terjadwal
'kategori_bantuan_id': kategoriBantuanId,
};

View File

@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PelaksanaanPenyaluranController extends GetxController {
// Instance Supabase Service
final SupabaseService supabaseService = SupabaseService.to;
// Controller untuk pencarian penerima
final TextEditingController searchPenerimaController =
TextEditingController();
// Data penerima penyaluran
final RxList<PenerimaPenyaluranModel> penerimaPenyaluran =
<PenerimaPenyaluranModel>[].obs;
final RxList<PenerimaPenyaluranModel> filteredPenerima =
<PenerimaPenyaluranModel>[].obs;
final RxInt jumlahPenerima = 0.obs;
final RxString filterStatus = 'SEMUA'.obs;
// Status loading
final isLoading = false.obs;
// Variabel untuk pencarian
final searchQuery = ''.obs;
// ID penyaluran yang sedang aktif
final RxString activePenyaluranId = ''.obs;
// Variabel untuk data skema bantuan
final Rx<SkemaBantuanModel?> skemaBantuan = Rx<SkemaBantuanModel?>(null);
final isLoadingSkema = false.obs;
// Variabel untuk data jadwal penyaluran
final Rx<PenyaluranBantuanModel?> jadwalPenyaluran =
Rx<PenyaluranBantuanModel?>(null);
final Rx<Map<String, dynamic>> jadwalPenyaluranFormatted =
Rx<Map<String, dynamic>>({});
final isLoadingJadwal = false.obs;
// Variabel untuk konfirmasi penerima
final RxBool isKonfirmasiChecked = false.obs;
final RxBool isIdentitasChecked = false.obs;
final RxBool isDataValidChecked = false.obs;
final RxString fotoBuktiPath = ''.obs;
final RxString tandaTanganPath = ''.obs;
final TextEditingController catatanController = TextEditingController();
@override
void onInit() {
super.onInit();
// Inisialisasi listener untuk filter status
ever(filterStatus, (_) => applyFilters());
}
@override
void onClose() {
// Bersihkan controller
searchPenerimaController.dispose();
catatanController.dispose();
super.onClose();
}
// Metode untuk memuat data jadwal penyaluran
Future<void> loadJadwalPenyaluran(String penyaluranId) async {
isLoadingJadwal.value = true;
jadwalPenyaluran.value = null;
jadwalPenyaluranFormatted.value = {};
try {
final response = await supabaseService.client
.from('penyaluran_bantuan')
.select('*, lokasi_penyaluran(*), kategori_bantuan(*)')
.eq('id', penyaluranId)
.single();
// Konversi ke model
final PenyaluranBantuanModel penyaluranModel =
PenyaluranBantuanModel.fromJson(response);
jadwalPenyaluran.value = penyaluranModel;
// Format data jadwal untuk tampilan
final Map<String, dynamic> formattedJadwal = {
'id': penyaluranModel.id,
'nama': penyaluranModel.nama,
'deskripsi': penyaluranModel.deskripsi,
'lokasi': response['lokasi_penyaluran'] != null
? response['lokasi_penyaluran']['nama']
: 'Tidak tersedia',
'kategori_bantuan': response['kategori_bantuan'] != null
? response['kategori_bantuan']['nama']
: 'Tidak tersedia',
'tanggal': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'waktu': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
'status': penyaluranModel.status,
'skema_bantuan_id': penyaluranModel.skemaId,
'lokasi_penyaluran_id': penyaluranModel.lokasiPenyaluranId,
'kategori_bantuan_id': penyaluranModel.kategoriBantuanId,
'raw_data': response, // Simpan data mentah untuk keperluan lain
};
jadwalPenyaluranFormatted.value = formattedJadwal;
// Jika ada ID skema, muat data skema bantuan
if (penyaluranModel.skemaId != null) {
loadSkemaBantuan(penyaluranModel.skemaId!);
}
print(
'DEBUG: Jadwal penyaluran berhasil dimuat: ${jadwalPenyaluran.value?.nama}');
} catch (e) {
print('DEBUG: Error saat memuat jadwal penyaluran: $e');
} finally {
isLoadingJadwal.value = false;
}
}
// Metode untuk memuat data skema bantuan
Future<void> loadSkemaBantuan(String skemaId) async {
isLoadingSkema.value = true;
try {
final response = await supabaseService.client
.from('xx02_skema_bantuan')
.select('*')
.eq('id', skemaId)
.single();
skemaBantuan.value = SkemaBantuanModel.fromJson(response);
print(
'DEBUG: Skema bantuan berhasil dimuat: ${skemaBantuan.value?.nama}');
} catch (e) {
print('DEBUG: Error saat memuat skema bantuan: $e');
} finally {
isLoadingSkema.value = false;
}
}
// Metode untuk memuat data penerima penyaluran
Future<void> loadPenerimaPenyaluran(String penyaluranId) async {
isLoading.value = true;
activePenyaluranId.value = penyaluranId;
try {
// Coba ambil data dari Supabase
final data = await _fetchPenerimaPenyaluranFromSupabase(penyaluranId);
if (data != null && data.isNotEmpty) {
// Konversi ke model
penerimaPenyaluran.value =
data.map((item) => PenerimaPenyaluranModel.fromJson(item)).toList();
jumlahPenerima.value = data.length;
print(
'Data penerima berhasil dimuat: ${penerimaPenyaluran.length} data');
}
// Terapkan filter
applyFilters();
} catch (e) {
print('Error saat memuat data penerima: $e');
applyFilters();
} finally {
isLoading.value = false;
}
}
// Metode untuk mengambil data penerima dari Supabase
Future<List<Map<String, dynamic>>?> _fetchPenerimaPenyaluranFromSupabase(
String penyaluranId) async {
try {
final response = await supabaseService.client
.from('penerima_penyaluran')
.select('*, warga(*)')
.eq('penyaluran_bantuan_id', penyaluranId);
return List<Map<String, dynamic>>.from(response);
} catch (e) {
print('Error saat mengambil data dari Supabase: $e');
return null;
}
}
// Metode untuk memfilter penerima berdasarkan kata kunci
void filterPenerima(String keyword) {
if (keyword.isEmpty) {
applyFilters();
return;
}
final lowercaseKeyword = keyword.toLowerCase();
final filtered = penerimaPenyaluran.where((penerima) {
final wargaData = penerima.warga ?? {};
final nama = ((wargaData['nama_lengkap'] ?? wargaData['nama']) ?? '')
.toString()
.toLowerCase();
final nik = (wargaData['nik'] ?? '').toString().toLowerCase();
final alamat = (wargaData['alamat'] ?? '').toString().toLowerCase();
final matches = nama.contains(lowercaseKeyword) ||
nik.contains(lowercaseKeyword) ||
alamat.contains(lowercaseKeyword);
return matches;
}).toList();
filteredPenerima.value = filtered;
}
// Metode untuk menerapkan filter status
void applyFilters() {
final keyword = searchPenerimaController.text.toLowerCase();
if (filterStatus.value == 'SEMUA' && keyword.isEmpty) {
filteredPenerima.value = penerimaPenyaluran;
return;
}
final filtered = penerimaPenyaluran.where((penerima) {
bool statusMatch = true;
if (filterStatus.value != 'SEMUA') {
statusMatch = penerima.statusPenerimaan == filterStatus.value;
}
if (keyword.isEmpty) return statusMatch;
final wargaData = penerima.warga ?? {};
final nama = ((wargaData['nama_lengkap'] ?? wargaData['nama']) ?? '')
.toString()
.toLowerCase();
final nik = (wargaData['nik'] ?? '').toString().toLowerCase();
final alamat = (wargaData['alamat'] ?? '').toString().toLowerCase();
final keywordMatch = nama.contains(keyword) ||
nik.contains(keyword) ||
alamat.contains(keyword);
return statusMatch && keywordMatch;
}).toList();
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);
// Jika berhasil, perbarui data lokal
if (result) {
await loadPenerimaPenyaluran(activePenyaluranId.value);
}
return result;
} catch (e) {
print('Error updating status penerimaan: $e');
return false;
}
}
// Metode untuk menyelesaikan jadwal penyaluran
Future<void> completeJadwal(String jadwalId) async {
try {
await supabaseService.completeJadwal(jadwalId);
} catch (e) {
print('Error completing jadwal: $e');
throw e.toString();
}
}
// Metode untuk mendapatkan warna status penerimaan
Color getStatusColor(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
return AppTheme.successColor;
case 'BELUMMENERIMA':
return AppTheme.warningColor;
default:
return Colors.grey;
}
}
// Metode untuk mendapatkan ikon status penerimaan
IconData getStatusIcon(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
return Icons.check_circle;
case 'BELUMMENERIMA':
return Icons.event_available;
default:
return Icons.info_outline;
}
}
// Metode untuk mendapatkan teks status penerimaan
String getStatusText(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
return 'Sudah Menerima';
case 'BELUMMENERIMA':
return 'Belum Menerima';
default:
return 'Status Tidak Diketahui';
}
}
// Metode untuk memilih foto bukti
void pilihFotoBukti() async {
// Implementasi untuk memilih foto dari galeri atau kamera
// Untuk sementara, gunakan URL dummy
fotoBuktiPath.value =
'https://via.placeholder.com/400x300?text=Bukti+Penyaluran';
}
// Metode untuk menghapus foto bukti
void hapusFotoBukti() {
fotoBuktiPath.value = '';
}
// Metode untuk membuka signature pad
void bukaSignaturePad(BuildContext context) {
// Implementasi untuk membuka signature pad
// Untuk sementara, gunakan URL dummy
tandaTanganPath.value =
'https://via.placeholder.com/400x200?text=Tanda+Tangan';
}
// Metode untuk menghapus tanda tangan
void hapusTandaTangan() {
tandaTanganPath.value = '';
}
// Metode untuk konfirmasi penyaluran
Future<void> konfirmasiPenyaluran(int penerimaId, String penyaluranId) async {
try {
isLoading.value = true;
// Simulasi proses konfirmasi
await Future.delayed(const Duration(seconds: 2));
// Reset form
isKonfirmasiChecked.value = false;
isIdentitasChecked.value = false;
isDataValidChecked.value = false;
fotoBuktiPath.value = '';
tandaTanganPath.value = '';
catatanController.clear();
// Perbarui status penerima di daftar
final index = penerimaPenyaluran
.indexWhere((penerima) => penerima.id == penerimaId);
if (index != -1) {
// Buat salinan model dengan status yang diperbarui
final updatedPenerima = PenerimaPenyaluranModel(
id: penerimaPenyaluran[index].id,
createdAt: penerimaPenyaluran[index].createdAt,
penyaluranBantuanId: penerimaPenyaluran[index].penyaluranBantuanId,
wargaId: penerimaPenyaluran[index].wargaId,
statusPenerimaan: 'SUDAHMENERIMA',
tanggalPenerimaan: penerimaPenyaluran[index].tanggalPenerimaan,
buktiPenerimaan: penerimaPenyaluran[index].buktiPenerimaan,
keterangan: penerimaPenyaluran[index].keterangan,
jumlahBantuan: penerimaPenyaluran[index].jumlahBantuan,
stokBantuanId: penerimaPenyaluran[index].stokBantuanId,
warga: penerimaPenyaluran[index].warga,
);
// Perbarui daftar
final List<PenerimaPenyaluranModel> updatedList =
List.from(penerimaPenyaluran);
updatedList[index] = updatedPenerima;
penerimaPenyaluran.value = updatedList;
// Terapkan filter
applyFilters();
}
// Tampilkan pesan sukses
Get.back();
Get.snackbar(
'Sukses',
'Konfirmasi penyaluran berhasil disimpan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menyimpan konfirmasi: $e',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,6 +1,6 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class PenerimaController extends GetxController {
final RxList<Map<String, dynamic>> daftarPenerima =
@ -190,8 +190,7 @@ class PenerimaController extends GetxController {
);
if (picked != null) {
tanggalPenyaluran.value =
DateFormat('dd MMMM yyyy', 'id_ID').format(picked);
tanggalPenyaluran.value = DateFormatter.formatDate(picked);
}
}

View File

@ -5,7 +5,7 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/detail_penitipan_dialog.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class DetailDonaturView extends GetView<DonaturController> {
const DetailDonaturView({super.key});
@ -207,8 +207,7 @@ class DetailDonaturView extends GetView<DonaturController> {
Icons.calendar_today,
'Terdaftar Sejak',
donatur.createdAt != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(donatur.createdAt!)
? DateFormatter.formatDate(donatur.createdAt!)
: 'Tidak diketahui',
),
],
@ -434,7 +433,7 @@ class DetailDonaturView extends GetView<DonaturController> {
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
final isUang = penitipan.isUang == true;
final tanggal = penitipan.createdAt != null
? DateFormat('dd MMM yyyy', 'id_ID').format(penitipan.createdAt!)
? DateFormatter.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy')
: 'Tanggal tidak diketahui';
String nilaiDonasi = '';

View File

@ -127,7 +127,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.green,
color: AppTheme.successColor,
borderRadius: BorderRadius.circular(20),
),
child: const Row(
@ -247,15 +247,15 @@ class DetailPenerimaView extends GetView<PenerimaController> {
switch (penerima['status']) {
case 'Selesai':
statusColor = Colors.green;
statusColor = AppTheme.completedColor;
statusIcon = Icons.check_circle;
break;
case 'Terjadwal':
statusColor = Colors.blue;
statusColor = AppTheme.processedColor;
statusIcon = Icons.event;
break;
case 'Belum disalurkan':
statusColor = Colors.orange;
statusColor = AppTheme.warningColor;
statusIcon = Icons.pending;
break;
default:
@ -412,7 +412,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
icon: const Icon(Icons.check_circle),
label: const Text('Konfirmasi Penyaluran'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(

View File

@ -268,16 +268,16 @@ class PengaduanView extends GetView<PetugasDesaController> {
IconData statusIcon;
switch (item['status']) {
case 'Diproses':
statusColor = Colors.orange;
statusIcon = Icons.pending_actions;
case 'MENUNGGU':
statusColor = AppTheme.warningColor;
statusIcon = Icons.pending;
break;
case 'Tindakan':
statusColor = Colors.blue;
statusIcon = Icons.engineering;
case 'DIPROSES':
statusColor = AppTheme.infoColor;
statusIcon = Icons.sync;
break;
case 'Selesai':
statusColor = Colors.green;
case 'SELESAI':
statusColor = AppTheme.successColor;
statusIcon = Icons.check_circle;
break;
default:

View File

@ -274,15 +274,15 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
switch (item.status) {
case 'MENUNGGU':
statusColor = Colors.orange;
statusIcon = Icons.pending_actions;
statusColor = AppTheme.warningColor;
statusIcon = Icons.pending;
break;
case 'TERVERIFIKASI':
statusColor = Colors.green;
statusColor = AppTheme.successColor;
statusIcon = Icons.check_circle;
break;
case 'DITOLAK':
statusColor = Colors.red;
statusColor = AppTheme.errorColor;
statusIcon = Icons.cancel;
break;
default:

View File

@ -73,11 +73,6 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
const SizedBox(height: 20),
// Ringkasan Permintaan Penjadwalan
PermintaanPenjadwalanSummaryWidget(controller: controller),
const SizedBox(height: 20),
// Jadwal hari ini
JadwalSectionWidget(
controller: controller,
@ -96,15 +91,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
status: 'Terjadwal',
),
const SizedBox(height: 20),
// Jadwal selesai
JadwalSectionWidget(
controller: controller,
title: 'Terlaksana',
jadwalList: controller.jadwalTerlaksana,
status: 'Terlaksana',
),
const SizedBox(height: 50),
],
);
}),

View File

@ -5,7 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penya
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
const PermintaanPenjadwalanView({Key? key}) : super(key: key);
const PermintaanPenjadwalanView({super.key});
@override
Widget build(BuildContext context) {
@ -239,7 +239,8 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
Widget _buildPermintaanItem(
BuildContext context, PenyaluranBantuanModel item) {
Color statusColor = Colors.orange;
// Status selalu MENUNGGU untuk permintaan penjadwalan
Color statusColor = AppTheme.warningColor;
IconData statusIcon = Icons.pending_actions;
return Container(

View File

@ -109,6 +109,22 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
);
}
// Tampilkan tombol riwayat jika tab Penyaluran aktif
if (activeTab == 1) {
return Row(
children: [
IconButton(
onPressed: () {
Get.toNamed('/petugas-desa/riwayat-penyaluran');
},
icon: const Icon(Icons.history),
tooltip: 'Riwayat Penyaluran',
),
notificationButton,
],
);
}
return notificationButton;
}),
],

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const RiwayatPenitipanView({super.key});
@ -186,11 +187,11 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
switch (item.status) {
case 'TERVERIFIKASI':
statusColor = Colors.green;
statusColor = AppTheme.successColor;
statusIcon = Icons.check_circle;
break;
case 'DITOLAK':
statusColor = Colors.red;
statusColor = AppTheme.errorColor;
statusIcon = Icons.cancel;
break;
default:

View File

@ -0,0 +1,392 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
const RiwayatPenyaluranView({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Riwayat Penyaluran'),
bottom: const TabBar(
tabs: [
Tab(text: 'Terlaksana'),
Tab(text: 'Batal Terlaksana'),
],
),
),
body: Obx(() => TabBarView(
children: [
// Tab Terlaksana
_buildPenyaluranList(context, 'TERLAKSANA'),
// Tab Batal Terlaksana
_buildPenyaluranList(context, 'BATALTERLAKSANA'),
],
)),
),
);
}
Widget _buildPenyaluranList(BuildContext context, String status) {
var filteredList = controller.jadwalTerlaksana
.where((item) => item.status == status)
.toList();
// Filter berdasarkan pencarian jika ada teks pencarian
final searchText = controller.searchController.text.toLowerCase();
if (searchText.isNotEmpty) {
filteredList = filteredList.where((item) {
final nama = item.nama?.toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
final lokasiNama = controller
.getLokasiPenyaluranName(item.lokasiPenyaluranId)
.toLowerCase();
final kategoriNama = controller
.getKategoriBantuanName(item.kategoriBantuanId)
.toLowerCase();
final tanggal =
DateFormatter.formatDateTime(item.tanggalPenyaluran).toLowerCase();
return nama.contains(searchText) ||
deskripsi.contains(searchText) ||
lokasiNama.contains(searchText) ||
kategoriNama.contains(searchText) ||
tanggal.contains(searchText);
}).toList();
}
return RefreshIndicator(
onRefresh: controller.refreshData,
child: controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search field
TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari riwayat penyaluran...',
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),
),
onChanged: (value) {
// Trigger rebuild dengan Obx
controller.update();
},
),
const SizedBox(height: 16),
// Info jumlah item
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'terlaksana' : 'batal terlaksana'}',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${DateFormatter.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
const SizedBox(height: 12),
// Informasi jumlah item dan terakhir update
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${DateFormatter.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
// Informasi terakhir update
Row(
children: [
Icon(Icons.update,
size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Update: ${DateFormatter.formatDateTimeWithHour(DateTime.now())}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
],
),
const SizedBox(height: 16),
// Daftar penyaluran
filteredList.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Icon(
Icons.inbox_outlined,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Tidak ada data penyaluran ${status == 'TERLAKSANA' ? 'terlaksana' : 'batal terlaksana'}',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
return _buildPenyaluranItem(
context, filteredList[index]);
},
),
],
),
),
),
);
}
Widget _buildPenyaluranItem(
BuildContext context, PenyaluranBantuanModel item) {
Color statusColor;
IconData statusIcon;
switch (item.status) {
case 'TERLAKSANA':
statusColor = AppTheme.completedColor;
statusIcon = Icons.check_circle;
break;
case 'BATALTERLAKSANA':
statusColor = AppTheme.errorColor;
statusIcon = Icons.cancel;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help_outline;
}
final lokasiNama =
controller.getLokasiPenyaluranName(item.lokasiPenyaluranId);
final kategoriNama =
controller.getKategoriBantuanName(item.kategoriBantuanId);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: statusColor.withOpacity(0.3),
width: 1,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Get.toNamed('/detail-penyaluran', arguments: item);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.nama ?? 'Penyaluran tanpa 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(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 16,
color: statusColor,
),
const SizedBox(width: 4),
Text(
item.status == 'TERLAKSANA' ? 'Terlaksana' : 'Batal',
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
item.deskripsi!,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
const Divider(height: 24),
_buildInfoItem(
Icons.location_on_outlined,
'Lokasi',
lokasiNama,
Theme.of(context).textTheme,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildInfoItem(
Icons.category_outlined,
'Kategori',
kategoriNama,
Theme.of(context).textTheme,
),
),
Expanded(
child: _buildInfoItem(
Icons.event,
'Tanggal',
DateFormatter.formatDateTime(item.tanggalPenyaluran,
defaultValue: 'Tidak ada tanggal'),
Theme.of(context).textTheme,
),
),
],
),
const SizedBox(height: 8),
_buildInfoItem(
Icons.people_outline,
'Jumlah Penerima',
'${DateFormatter.formatNumber(item.jumlahPenerima ?? 0)} orang',
Theme.of(context).textTheme,
),
if (item.alasanPembatalan != null &&
item.alasanPembatalan!.isNotEmpty) ...[
const SizedBox(height: 8),
_buildInfoItem(
Icons.info_outline,
'Alasan Pembatalan',
item.alasanPembatalan!,
Theme.of(context).textTheme,
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
Get.toNamed('/detail-penyaluran', arguments: item);
},
icon: const Icon(Icons.visibility_outlined),
label: const Text('Lihat Detail'),
style: TextButton.styleFrom(
foregroundColor: statusColor,
),
),
],
),
],
),
),
),
);
}
Widget _buildInfoItem(
IconData icon,
String label,
String value,
TextTheme textTheme,
) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
Text(
value,
style: textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
],
);
}
}

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
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/utils/date_formatter.dart';
class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
const TambahPenyaluranView({super.key});
@ -27,8 +27,6 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
final TextEditingController tanggalPenyaluranController =
TextEditingController();
final TextEditingController waktuMulaiController = TextEditingController();
final TextEditingController waktuSelesaiController =
TextEditingController();
// Variabel untuk menyimpan nilai yang dipilih
final Rx<String?> selectedSkemaBantuanId = Rx<String?>(null);
@ -40,7 +38,6 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
// Tanggal dan waktu penyaluran
final Rx<DateTime?> selectedDate = Rx<DateTime?>(null);
final Rx<TimeOfDay?> selectedWaktuMulai = Rx<TimeOfDay?>(null);
final Rx<TimeOfDay?> selectedWaktuSelesai = Rx<TimeOfDay?>(null);
// Fungsi untuk memuat data pengajuan kelayakan bantuan
Future<void> loadPengajuanKelayakan(String skemaId) async {
@ -209,7 +206,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 16),
const SizedBox(height: 8),
Obx(() => jumlahPenerima.value > 0
? TextButton.icon(
onPressed: () async {
@ -295,7 +292,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
label: const Text('Lihat Daftar Penerima'),
)
: const SizedBox.shrink()),
const SizedBox(height: 16),
const SizedBox(height: 8),
// Tanggal Penyaluran
Text(
@ -330,7 +327,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
if (pickedDate != null) {
selectedDate.value = pickedDate;
tanggalPenyaluranController.text =
DateFormat('dd MMMM yyyy', 'id_ID').format(pickedDate);
DateFormatter.formatDate(pickedDate);
}
},
validator: (value) {
@ -372,152 +369,43 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
),
const SizedBox(height: 16),
// Rentang Waktu Penyaluran
Text(
'Rentang Waktu Penyaluran',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
// Waktu Mulai
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Waktu Mulai'),
const SizedBox(height: 4),
TextFormField(
controller: waktuMulaiController,
readOnly: true,
decoration: InputDecoration(
hintText: 'Mulai',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Icons.access_time),
),
onTap: () async {
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime != null) {
selectedWaktuMulai.value = pickedTime;
waktuMulaiController.text =
'${pickedTime.hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')}';
// Jika waktu selesai belum dipilih atau lebih awal dari waktu mulai
if (selectedWaktuSelesai.value == null ||
(selectedWaktuSelesai.value != null &&
(pickedTime.hour >
selectedWaktuSelesai
.value!.hour ||
(pickedTime.hour ==
selectedWaktuSelesai
.value!.hour &&
pickedTime.minute >=
selectedWaktuSelesai
.value!.minute)))) {
// Set waktu selesai 1 jam setelah waktu mulai
final TimeOfDay defaultSelesai = TimeOfDay(
hour: (pickedTime.hour + 1) % 24,
minute: pickedTime.minute,
);
selectedWaktuSelesai.value = defaultSelesai;
waktuSelesaiController.text =
'${defaultSelesai.hour.toString().padLeft(2, '0')}:${defaultSelesai.minute.toString().padLeft(2, '0')}';
}
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Waktu mulai harus dipilih';
}
return null;
},
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Waktu Selesai'),
const SizedBox(height: 4),
TextFormField(
controller: waktuSelesaiController,
readOnly: true,
decoration: InputDecoration(
hintText: 'Selesai',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Icons.access_time),
),
onTap: () async {
// Pastikan waktu mulai sudah dipilih
if (selectedWaktuMulai.value == null) {
Get.snackbar(
'Perhatian',
'Silakan pilih waktu mulai terlebih dahulu',
backgroundColor: Colors.amber,
colorText: Colors.black,
);
return;
}
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: selectedWaktuSelesai.value ??
TimeOfDay(
hour: (selectedWaktuMulai.value!.hour + 1) %
24,
minute: selectedWaktuMulai.value!.minute,
),
);
if (pickedTime != null) {
// Validasi waktu selesai harus setelah waktu mulai
if (pickedTime.hour <
selectedWaktuMulai.value!.hour ||
(pickedTime.hour ==
selectedWaktuMulai.value!.hour &&
pickedTime.minute <=
selectedWaktuMulai.value!.minute)) {
Get.snackbar(
'Perhatian',
'Waktu selesai harus setelah waktu mulai',
backgroundColor: Colors.amber,
colorText: Colors.black,
);
return;
}
selectedWaktuSelesai.value = pickedTime;
waktuSelesaiController.text =
'${pickedTime.hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')}';
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Waktu selesai harus dipilih';
}
return null;
},
),
],
const Text('Waktu Mulai'),
const SizedBox(height: 4),
TextFormField(
controller: waktuMulaiController,
readOnly: true,
decoration: InputDecoration(
hintText: 'Pilih waktu mulai',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Icons.access_time),
),
onTap: () async {
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime != null) {
selectedWaktuMulai.value = pickedTime;
waktuMulaiController.text =
'${pickedTime.hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')}';
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Waktu mulai harus dipilih';
}
return null;
},
),
],
),
@ -570,19 +458,6 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
).toLocal();
}
// Gabungkan tanggal dan waktu selesai
DateTime? tanggalWaktuSelesai;
if (selectedDate.value != null &&
selectedWaktuSelesai.value != null) {
tanggalWaktuSelesai = DateTime(
selectedDate.value!.year,
selectedDate.value!.month,
selectedDate.value!.day,
selectedWaktuSelesai.value!.hour,
selectedWaktuSelesai.value!.minute,
).toLocal();
}
// Panggil fungsi untuk menambahkan penyaluran
controller.tambahPenyaluran(
nama: namaController.text,
@ -591,7 +466,6 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
lokasiPenyaluranId: selectedLokasiPenyaluranId.value!,
jumlahPenerima: jumlahPenerima.value,
tanggalPenyaluran: tanggalWaktuMulai,
tanggalWaktuSelesai: tanggalWaktuSelesai,
kategoriBantuanId:
selectedSkemaBantuan.value!.kategoriBantuanId!,
);