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:
@ -22,8 +22,18 @@ class DetailPenyaluranController extends GetxController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final String? penyaluranId = Get.parameters['id'];
|
||||
print('DetailPenyaluranController - ID Penyaluran: $penyaluranId');
|
||||
if (penyaluranId != null) {
|
||||
final PenyaluranBantuanModel? penyaluranData =
|
||||
Get.arguments as PenyaluranBantuanModel?;
|
||||
|
||||
if (penyaluranData != null) {
|
||||
// Jika data penyaluran diterima langsung dari argumen
|
||||
penyaluran.value = penyaluranData;
|
||||
if (penyaluran.value?.id != null) {
|
||||
loadPenyaluranDetails(penyaluran.value!.id!);
|
||||
}
|
||||
checkUserRole();
|
||||
} else if (penyaluranId != null) {
|
||||
// Jika hanya ID penyaluran yang diterima
|
||||
loadPenyaluranData(penyaluranId);
|
||||
checkUserRole();
|
||||
} else {
|
||||
@ -42,7 +52,7 @@ class DetailPenyaluranController extends GetxController {
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (userData != null && userData['role'] == 'petugas_desa') {
|
||||
if (userData['role'] == 'petugas_desa') {
|
||||
isPetugasDesa.value = true;
|
||||
}
|
||||
}
|
||||
@ -66,90 +76,84 @@ class DetailPenyaluranController extends GetxController {
|
||||
|
||||
print('DetailPenyaluranController - Data penyaluran: $penyaluranData');
|
||||
|
||||
if (penyaluranData != null) {
|
||||
// Pastikan data yang diterima sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedData =
|
||||
Map<String, dynamic>.from(penyaluranData);
|
||||
// Pastikan data yang diterima sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedData =
|
||||
Map<String, dynamic>.from(penyaluranData);
|
||||
|
||||
// Konversi jumlah_penerima ke int jika bertipe String
|
||||
if (sanitizedData['jumlah_penerima'] is String) {
|
||||
sanitizedData['jumlah_penerima'] =
|
||||
int.tryParse(sanitizedData['jumlah_penerima'] as String) ?? 0;
|
||||
}
|
||||
// Konversi jumlah_penerima ke int jika bertipe String
|
||||
if (sanitizedData['jumlah_penerima'] is String) {
|
||||
sanitizedData['jumlah_penerima'] =
|
||||
int.tryParse(sanitizedData['jumlah_penerima'] as String) ?? 0;
|
||||
}
|
||||
|
||||
penyaluran.value = PenyaluranBantuanModel.fromJson(sanitizedData);
|
||||
penyaluran.value = PenyaluranBantuanModel.fromJson(sanitizedData);
|
||||
print(
|
||||
'DetailPenyaluranController - Model penyaluran: ${penyaluran.value?.nama}');
|
||||
|
||||
// Ambil data skema bantuan jika ada
|
||||
if (penyaluran.value?.skemaId != null &&
|
||||
penyaluran.value!.skemaId!.isNotEmpty) {
|
||||
print(
|
||||
'DetailPenyaluranController - Model penyaluran: ${penyaluran.value?.nama}');
|
||||
'DetailPenyaluranController - Memuat skema bantuan dengan ID: ${penyaluran.value!.skemaId}');
|
||||
final skemaData = await _supabaseService.client
|
||||
.from('xx02_skema_bantuan')
|
||||
.select('*')
|
||||
.eq('id', penyaluran.value!.skemaId!)
|
||||
.single();
|
||||
|
||||
// Ambil data skema bantuan jika ada
|
||||
if (penyaluran.value?.skemaId != null &&
|
||||
penyaluran.value!.skemaId!.isNotEmpty) {
|
||||
print(
|
||||
'DetailPenyaluranController - Memuat skema bantuan dengan ID: ${penyaluran.value!.skemaId}');
|
||||
final skemaData = await _supabaseService.client
|
||||
.from('xx02_skema_bantuan')
|
||||
.select('*')
|
||||
.eq('id', penyaluran.value!.skemaId!)
|
||||
.single();
|
||||
print('DetailPenyaluranController - Data skema bantuan: $skemaData');
|
||||
if (skemaData != null) {
|
||||
// Pastikan data skema sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedSkemaData =
|
||||
Map<String, dynamic>.from(skemaData);
|
||||
|
||||
print('DetailPenyaluranController - Data skema bantuan: $skemaData');
|
||||
if (skemaData != null) {
|
||||
// Pastikan data skema sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedSkemaData =
|
||||
Map<String, dynamic>.from(skemaData);
|
||||
|
||||
// Konversi kuota ke int jika bertipe String
|
||||
if (sanitizedSkemaData['kuota'] is String) {
|
||||
sanitizedSkemaData['kuota'] =
|
||||
int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0;
|
||||
}
|
||||
|
||||
// Konversi petugas_verifikasi_id ke int jika bertipe String
|
||||
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] as String);
|
||||
}
|
||||
|
||||
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
|
||||
print(
|
||||
'DetailPenyaluranController - Model skema bantuan: ${skemaBantuan.value?.nama}');
|
||||
// Konversi kuota ke int jika bertipe String
|
||||
if (sanitizedSkemaData['kuota'] is String) {
|
||||
sanitizedSkemaData['kuota'] =
|
||||
int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0;
|
||||
}
|
||||
|
||||
// Konversi petugas_verifikasi_id ke int jika bertipe String
|
||||
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] as String);
|
||||
}
|
||||
|
||||
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
|
||||
print(
|
||||
'DetailPenyaluranController - Model skema bantuan: ${skemaBantuan.value?.nama}');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data penerima penyaluran
|
||||
final penerimaPenyaluranData = await _supabaseService.client
|
||||
.from('penerima_penyaluran')
|
||||
.select('*, warga:warga_id(*)')
|
||||
.eq('penyaluran_bantuan_id', penyaluranId);
|
||||
// Ambil data penerima penyaluran
|
||||
final penerimaPenyaluranData = await _supabaseService.client
|
||||
.from('penerima_penyaluran')
|
||||
.select('*, warga:warga_id(*)')
|
||||
.eq('penyaluran_bantuan_id', penyaluranId);
|
||||
|
||||
print(
|
||||
'DetailPenyaluranController - Data penerima penyaluran: $penerimaPenyaluranData');
|
||||
if (penerimaPenyaluranData != null) {
|
||||
final List<PenerimaPenyaluranModel> penerima = [];
|
||||
for (var item in penerimaPenyaluranData) {
|
||||
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedPenerimaData =
|
||||
Map<String, dynamic>.from(item);
|
||||
|
||||
// Konversi jumlah_bantuan ke double jika bertipe String
|
||||
if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
|
||||
sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse(
|
||||
sanitizedPenerimaData['jumlah_bantuan'] as String);
|
||||
}
|
||||
|
||||
penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
|
||||
}
|
||||
penerimaPenyaluran.assignAll(penerima);
|
||||
print(
|
||||
'DetailPenyaluranController - Data penerima penyaluran: $penerimaPenyaluranData');
|
||||
if (penerimaPenyaluranData != null) {
|
||||
final List<PenerimaPenyaluranModel> penerima = [];
|
||||
for (var item in penerimaPenyaluranData) {
|
||||
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedPenerimaData =
|
||||
Map<String, dynamic>.from(item);
|
||||
'DetailPenyaluranController - Jumlah penerima: ${penerima.length}');
|
||||
|
||||
// Konversi id ke int jika bertipe String
|
||||
if (sanitizedPenerimaData['id'] is String) {
|
||||
sanitizedPenerimaData['id'] =
|
||||
int.tryParse(sanitizedPenerimaData['id'] as String);
|
||||
}
|
||||
|
||||
// Konversi jumlah_bantuan ke double jika bertipe String
|
||||
if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
|
||||
sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse(
|
||||
sanitizedPenerimaData['jumlah_bantuan'] as String);
|
||||
}
|
||||
|
||||
penerima
|
||||
.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
|
||||
}
|
||||
penerimaPenyaluran.assignAll(penerima);
|
||||
print(
|
||||
'DetailPenyaluranController - Jumlah penerima: ${penerima.length}');
|
||||
}
|
||||
//print id
|
||||
print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading penyaluran data: $e');
|
||||
@ -165,7 +169,8 @@ class DetailPenyaluranController extends GetxController {
|
||||
|
||||
Future<void> refreshData() async {
|
||||
if (penyaluran.value?.id != null) {
|
||||
await loadPenyaluranData(penyaluran.value!.id!);
|
||||
// Jika data penyaluran sudah ada, cukup muat detail saja
|
||||
await loadPenyaluranDetails(penyaluran.value!.id!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,10 +183,10 @@ class DetailPenyaluranController extends GetxController {
|
||||
throw Exception('ID penyaluran tidak ditemukan');
|
||||
}
|
||||
|
||||
// Update status penyaluran menjadi "BERLANGSUNG"
|
||||
// Update status penyaluran menjadi "AKTIF"
|
||||
await _supabaseService.client
|
||||
.from('penyaluran_bantuan')
|
||||
.update({'status': 'BERLANGSUNG'}).eq('id', penyaluran.value!.id!);
|
||||
.update({'status': 'AKTIF'}).eq('id', penyaluran.value!.id!);
|
||||
|
||||
await refreshData();
|
||||
|
||||
@ -208,7 +213,7 @@ class DetailPenyaluranController extends GetxController {
|
||||
|
||||
// Fungsi untuk konfirmasi penerimaan bantuan oleh penerima
|
||||
Future<void> konfirmasiPenerimaan(PenerimaPenyaluranModel penerima,
|
||||
{String? buktiPenerimaan}) async {
|
||||
{required String buktiPenerimaan, required String tandaTangan}) async {
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
@ -216,39 +221,39 @@ class DetailPenyaluranController extends GetxController {
|
||||
throw Exception('ID penerima tidak ditemukan');
|
||||
}
|
||||
|
||||
if (buktiPenerimaan.isEmpty) {
|
||||
throw Exception('Bukti penerimaan tidak boleh kosong');
|
||||
}
|
||||
|
||||
if (tandaTangan.isEmpty) {
|
||||
throw Exception('Tanda tangan tidak boleh kosong');
|
||||
}
|
||||
|
||||
// Update status penerimaan menjadi "DITERIMA"
|
||||
final Map<String, dynamic> updateData = {
|
||||
'status_penerimaan': 'DITERIMA',
|
||||
'tanggal_penerimaan': DateTime.now().toIso8601String(),
|
||||
'bukti_penerimaan': buktiPenerimaan,
|
||||
'tanda_tangan': tandaTangan,
|
||||
};
|
||||
|
||||
if (buktiPenerimaan != null) {
|
||||
updateData['bukti_penerimaan'] = buktiPenerimaan;
|
||||
}
|
||||
print(
|
||||
'DetailPenyaluranController - Updating penerima with ID: ${penerima.id}');
|
||||
print('DetailPenyaluranController - Update data: $updateData');
|
||||
|
||||
await _supabaseService.client
|
||||
.from('penerima_penyaluran')
|
||||
.update(updateData)
|
||||
.eq('id', penerima.id!);
|
||||
|
||||
// Refresh data setelah konfirmasi berhasil
|
||||
await refreshData();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Konfirmasi penerimaan bantuan berhasil',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
// Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima
|
||||
} catch (e) {
|
||||
print('Error konfirmasi penerimaan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat konfirmasi penerimaan bantuan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
// Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima
|
||||
rethrow; // Melempar kembali exception agar dapat ditangkap di _konfirmasiPenerimaan
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
@ -331,9 +336,9 @@ class DetailPenyaluranController extends GetxController {
|
||||
throw Exception('ID penyaluran tidak ditemukan');
|
||||
}
|
||||
|
||||
// Update status penyaluran menjadi "DIBATALKAN"
|
||||
// Update status penyaluran menjadi "BATALTERLAKSANA"
|
||||
await _supabaseService.client.from('penyaluran_bantuan').update({
|
||||
'status': 'DIBATALKAN',
|
||||
'status': 'BATALTERLAKSANA',
|
||||
'alasan_pembatalan': alasan,
|
||||
'tanggal_selesai': DateTime.now().toIso8601String(),
|
||||
}).eq('id', penyaluran.value!.id!);
|
||||
@ -361,36 +366,140 @@ class DetailPenyaluranController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk mengupload bukti penerimaan
|
||||
Future<String?> uploadBuktiPenerimaan(String filePath) async {
|
||||
// Fungsi untuk mengupload bukti penerimaan atau tanda tangan
|
||||
Future<String> uploadBuktiPenerimaan(String filePath,
|
||||
{bool isTandaTangan = false}) async {
|
||||
try {
|
||||
final fileName =
|
||||
'bukti_penerimaan_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final String folderName =
|
||||
isTandaTangan ? 'tanda_tangan' : 'bukti_penerimaan';
|
||||
final String filePrefix =
|
||||
isTandaTangan ? 'tanda_tangan' : 'bukti_penerimaan';
|
||||
final String fileName =
|
||||
'${filePrefix}_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final file = File(filePath);
|
||||
|
||||
print(
|
||||
'Uploading ${isTandaTangan ? "tanda tangan" : "bukti penerimaan"} dari: $filePath');
|
||||
print('File exists: ${file.existsSync()}');
|
||||
print('File size: ${file.lengthSync()} bytes');
|
||||
|
||||
if (!file.existsSync()) {
|
||||
throw Exception('File tidak ditemukan: $filePath');
|
||||
}
|
||||
|
||||
print('Uploading ke bucket: $folderName dengan nama file: $fileName');
|
||||
final storageResponse = await _supabaseService.client.storage
|
||||
.from('bukti_penerimaan')
|
||||
.from(folderName)
|
||||
.upload(fileName, file);
|
||||
|
||||
print('Storage response: $storageResponse');
|
||||
if (storageResponse.isEmpty) {
|
||||
throw Exception('Gagal mengupload bukti penerimaan');
|
||||
throw Exception(
|
||||
'Gagal mengupload ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}');
|
||||
}
|
||||
|
||||
final fileUrl = _supabaseService.client.storage
|
||||
.from('bukti_penerimaan')
|
||||
.from(folderName)
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
print('File URL: $fileUrl');
|
||||
if (fileUrl.isEmpty) {
|
||||
throw Exception(
|
||||
'Gagal mendapatkan URL ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}');
|
||||
}
|
||||
|
||||
return fileUrl;
|
||||
} catch (e) {
|
||||
print('Error upload bukti penerimaan: $e');
|
||||
print(
|
||||
'Error upload ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}: $e');
|
||||
// Tidak perlu menampilkan snackbar di sini karena sudah ditampilkan di halaman konfirmasi penerima
|
||||
throw Exception(
|
||||
'Gagal mengupload ${isTandaTangan ? 'tanda tangan' : 'bukti penerimaan'}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk memuat detail penyaluran (skema dan penerima) tanpa memuat ulang data penyaluran
|
||||
Future<void> loadPenyaluranDetails(String penyaluranId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
print(
|
||||
'DetailPenyaluranController - Memuat detail penyaluran dengan ID: $penyaluranId');
|
||||
|
||||
// Ambil data skema bantuan jika ada
|
||||
if (penyaluran.value?.skemaId != null &&
|
||||
penyaluran.value!.skemaId!.isNotEmpty) {
|
||||
print(
|
||||
'DetailPenyaluranController - Memuat skema bantuan dengan ID: ${penyaluran.value!.skemaId}');
|
||||
final skemaData = await _supabaseService.client
|
||||
.from('xx02_skema_bantuan')
|
||||
.select('*')
|
||||
.eq('id', penyaluran.value!.skemaId!)
|
||||
.single();
|
||||
|
||||
print('DetailPenyaluranController - Data skema bantuan: $skemaData');
|
||||
if (skemaData != null) {
|
||||
// Pastikan data skema sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedSkemaData =
|
||||
Map<String, dynamic>.from(skemaData);
|
||||
|
||||
// Konversi kuota ke int jika bertipe String
|
||||
if (sanitizedSkemaData['kuota'] is String) {
|
||||
sanitizedSkemaData['kuota'] =
|
||||
int.tryParse(sanitizedSkemaData['kuota'] as String) ?? 0;
|
||||
}
|
||||
|
||||
// Konversi petugas_verifikasi_id ke int jika bertipe String
|
||||
if (sanitizedSkemaData['petugas_verifikasi_id'] is String) {
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] = int.tryParse(
|
||||
sanitizedSkemaData['petugas_verifikasi_id'] as String);
|
||||
}
|
||||
|
||||
skemaBantuan.value = SkemaBantuanModel.fromJson(sanitizedSkemaData);
|
||||
print(
|
||||
'DetailPenyaluranController - Model skema bantuan: ${skemaBantuan.value?.nama}');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data penerima penyaluran
|
||||
final penerimaPenyaluranData = await _supabaseService.client
|
||||
.from('penerima_penyaluran')
|
||||
.select('*, warga:warga_id(*)')
|
||||
.eq('penyaluran_bantuan_id', penyaluranId);
|
||||
|
||||
print(
|
||||
'DetailPenyaluranController - Data penerima penyaluran: $penerimaPenyaluranData');
|
||||
if (penerimaPenyaluranData != null) {
|
||||
final List<PenerimaPenyaluranModel> penerima = [];
|
||||
for (var item in penerimaPenyaluranData) {
|
||||
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
|
||||
Map<String, dynamic> sanitizedPenerimaData =
|
||||
Map<String, dynamic>.from(item);
|
||||
|
||||
// Konversi jumlah_bantuan ke double jika bertipe String
|
||||
if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
|
||||
sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse(
|
||||
sanitizedPenerimaData['jumlah_bantuan'] as String);
|
||||
}
|
||||
|
||||
penerima.add(PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData));
|
||||
}
|
||||
penerimaPenyaluran.assignAll(penerima);
|
||||
print(
|
||||
'DetailPenyaluranController - Jumlah penerima: ${penerima.length}');
|
||||
|
||||
if (penerima.isNotEmpty) {
|
||||
print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading penyaluran details: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat mengupload bukti penerimaan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
'Terjadi kesalahan saat memuat detail penyaluran',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/detail_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/konfirmasi_penerima_page.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_formatter.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
|
||||
class DetailPenyaluranPage extends StatelessWidget {
|
||||
final controller = Get.put(DetailPenyaluranController());
|
||||
@ -49,19 +49,34 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
_buildPenerimaPenyaluranSection(context),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => _buildActionButtons(context)),
|
||||
// Menampilkan section alasan pembatalan jika status BATALTERLAKSANA
|
||||
if (controller.penyaluran.value?.status?.toUpperCase() ==
|
||||
'BATALTERLAKSANA' &&
|
||||
controller.penyaluran.value?.alasanPembatalan != null &&
|
||||
controller.penyaluran.value!.alasanPembatalan!.isNotEmpty)
|
||||
_buildPembatalanSection(context),
|
||||
const SizedBox(height: 24),
|
||||
// Tombol aksi akan ditampilkan di bottomNavigationBar
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
bottomNavigationBar: Obx(() {
|
||||
final status = controller.penyaluran.value?.status?.toUpperCase() ?? '';
|
||||
if (status == 'AKTIF' ||
|
||||
status == 'DISETUJUI' ||
|
||||
status == 'DIJADWALKAN') {
|
||||
return _buildActionButtons(context);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context) {
|
||||
final penyaluran = controller.penyaluran.value!;
|
||||
final skema = controller.skemaBantuan.value;
|
||||
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
@ -95,8 +110,17 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
_buildInfoRow(
|
||||
'Tanggal',
|
||||
penyaluran.tanggalPenyaluran != null
|
||||
? dateFormat.format(penyaluran.tanggalPenyaluran!)
|
||||
? DateFormatter.formatDateTime(
|
||||
penyaluran.tanggalPenyaluran!)
|
||||
: 'Belum dijadwalkan'),
|
||||
// Tampilkan tanggal selesai jika status TERLAKSANA atau BATALTERLAKSANA
|
||||
if (penyaluran.status == 'TERLAKSANA' ||
|
||||
penyaluran.status == 'BATALTERLAKSANA')
|
||||
_buildInfoRow(
|
||||
'Tanggal Selesai',
|
||||
penyaluran.tanggalSelesai != null
|
||||
? DateFormatter.formatDateTime(penyaluran.tanggalSelesai!)
|
||||
: '-'),
|
||||
_buildInfoRow(
|
||||
'Jumlah Penerima', '${penyaluran.jumlahPenerima ?? 0} orang'),
|
||||
|
||||
@ -128,24 +152,69 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
],
|
||||
|
||||
// Alasan penolakan jika ada
|
||||
if (penyaluran.alasanPenolakan != null &&
|
||||
penyaluran.alasanPenolakan!.isNotEmpty) ...[
|
||||
if (penyaluran.alasanPembatalan != null &&
|
||||
penyaluran.alasanPembatalan!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Alasan Penolakan:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red[700],
|
||||
if (penyaluran.status?.toUpperCase() == 'BATALTERLAKSANA') ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.errorColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppTheme.errorColor.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cancel_outlined,
|
||||
color: AppTheme.errorColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Alasan Pembatalan:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.errorColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
penyaluran.alasanPembatalan!,
|
||||
style: TextStyle(
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
penyaluran.alasanPenolakan!,
|
||||
style: TextStyle(
|
||||
color: Colors.red[700],
|
||||
fontStyle: FontStyle.italic,
|
||||
] else ...[
|
||||
Text(
|
||||
'Alasan Pembatalan:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
penyaluran.alasanPembatalan!,
|
||||
style: TextStyle(
|
||||
color: Colors.red[700],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -345,23 +414,16 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
String statusText = _getStatusText(status);
|
||||
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU':
|
||||
case 'DIJADWALKAN':
|
||||
backgroundColor = AppTheme.processedColor;
|
||||
break;
|
||||
case 'DISETUJUI':
|
||||
backgroundColor = AppTheme.verifiedColor;
|
||||
textColor = Colors.black87;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
backgroundColor = AppTheme.rejectedColor;
|
||||
break;
|
||||
case 'AKTIF':
|
||||
backgroundColor = AppTheme.scheduledColor;
|
||||
break;
|
||||
case 'TERLAKSANA':
|
||||
backgroundColor = AppTheme.completedColor;
|
||||
break;
|
||||
case 'DIBATALKAN':
|
||||
case 'BATALTERLAKSANA':
|
||||
backgroundColor = AppTheme.errorColor;
|
||||
break;
|
||||
default:
|
||||
@ -391,7 +453,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
String statusText = _getStatusPenerimaanText(status);
|
||||
|
||||
// Konversi status ke format yang diinginkan
|
||||
if (status.toUpperCase() == 'SUDAHMENERIMA') {
|
||||
if (status.toUpperCase() == 'DITERIMA') {
|
||||
backgroundColor = AppTheme.successColor;
|
||||
statusText = 'Sudah Menerima';
|
||||
} else {
|
||||
@ -421,67 +483,71 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
final status = controller.penyaluran.value?.status?.toUpperCase() ?? '';
|
||||
|
||||
if (controller.isProcessing.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Jika status DISETUJUI, tampilkan tombol Mulai Penyaluran
|
||||
if (status == 'DISETUJUI') {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text('Mulai Penyaluran'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: controller.mulaiPenyaluran,
|
||||
// Container untuk tombol-tombol
|
||||
Widget buildButtonContainer(List<Widget> children) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, -3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Jika status AKTIF, tampilkan tombol Selesaikan Penyaluran dan Batalkan
|
||||
// Tombol Batalkan yang digunakan berulang
|
||||
Widget cancelButton = Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Batalkan'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorColor,
|
||||
side: const BorderSide(color: AppTheme.errorColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: () => _showBatalkanDialog(context),
|
||||
),
|
||||
);
|
||||
|
||||
if (status == 'AKTIF') {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Selesaikan Penyaluran'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: controller.selesaikanPenyaluran,
|
||||
return buildButtonContainer([
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Selesaikan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: controller.selesaikanPenyaluran,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Batalkan Penyaluran'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorColor,
|
||||
side: const BorderSide(color: AppTheme.errorColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: () => _showBatalkanDialog(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
cancelButton,
|
||||
]);
|
||||
} else if (status == 'DIJADWALKAN') {
|
||||
return buildButtonContainer([cancelButton]);
|
||||
}
|
||||
|
||||
// Jika status TERLAKSANA atau DIBATALKAN, tidak perlu menampilkan tombol aksi
|
||||
// Untuk status lainnya tidak menampilkan tombol
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@ -593,7 +659,6 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
|
||||
void _showDetailPenerima(
|
||||
BuildContext context, PenerimaPenyaluranModel penerima) {
|
||||
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
final warga = penerima.warga;
|
||||
|
||||
showModalBottomSheet(
|
||||
@ -646,7 +711,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
_getStatusPenerimaanText(penerima.statusPenerimaan ?? '-')),
|
||||
if (penerima.tanggalPenerimaan != null)
|
||||
_buildInfoRow('Tanggal Penerimaan',
|
||||
dateFormat.format(penerima.tanggalPenerimaan!)),
|
||||
DateFormatter.formatDate(penerima.tanggalPenerimaan!)),
|
||||
if (penerima.jumlahBantuan != null)
|
||||
_buildInfoRow(
|
||||
'Jumlah Bantuan', penerima.jumlahBantuan.toString()),
|
||||
@ -688,8 +753,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
const SizedBox(height: 30),
|
||||
if (controller.penyaluran.value?.status?.toUpperCase() ==
|
||||
'AKTIF' &&
|
||||
penerima.statusPenerimaan?.toUpperCase() !=
|
||||
'SUDAHMENERIMA') ...[
|
||||
penerima.statusPenerimaan?.toUpperCase() != 'DITERIMA') ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@ -736,18 +800,14 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU':
|
||||
return 'Menunggu Persetujuan';
|
||||
case 'DISETUJUI':
|
||||
return 'Disetujui';
|
||||
case 'DITOLAK':
|
||||
return 'Ditolak';
|
||||
case 'DIJADWALKAN':
|
||||
return 'Terjadwal';
|
||||
case 'AKTIF':
|
||||
return 'Sedang AKTIF';
|
||||
return 'Aktif';
|
||||
case 'TERLAKSANA':
|
||||
return 'Terlaksana';
|
||||
case 'DIBATALKAN':
|
||||
return 'Dibatalkan';
|
||||
case 'BATALTERLAKSANA':
|
||||
return 'Batal Terlaksana';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
@ -755,11 +815,82 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
|
||||
String _getStatusPenerimaanText(String status) {
|
||||
// Konversi status ke format yang diinginkan
|
||||
if (status.toUpperCase() == 'SUDAHMENERIMA') {
|
||||
if (status.toUpperCase() == 'DITERIMA') {
|
||||
return 'Sudah Menerima';
|
||||
} else {
|
||||
// Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA
|
||||
return 'Belum Menerima';
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPembatalanSection(BuildContext context) {
|
||||
final penyaluran = controller.penyaluran.value!;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: AppTheme.errorColor.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.cancel_outlined,
|
||||
color: AppTheme.errorColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Informasi Pembatalan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow('Status', 'Batal Terlaksana'),
|
||||
if (penyaluran.tanggalSelesai != null)
|
||||
_buildInfoRow('Tanggal Pembatalan',
|
||||
DateFormatter.formatDateTime(penyaluran.tanggalSelesai!)),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Alasan Pembatalan:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.errorColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
penyaluran.alasanPembatalan!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/bentuk_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/detail_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:signature/signature.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:penyaluran_app/app/utils/date_formatter.dart';
|
||||
|
||||
class KonfirmasiPenerimaPage extends StatefulWidget {
|
||||
final PenerimaPenyaluranModel penerima;
|
||||
@ -15,12 +18,12 @@ class KonfirmasiPenerimaPage extends StatefulWidget {
|
||||
final DateTime? tanggalPenyaluran;
|
||||
|
||||
const KonfirmasiPenerimaPage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.penerima,
|
||||
this.bentukBantuan,
|
||||
this.jumlahBantuan,
|
||||
this.tanggalPenyaluran,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<KonfirmasiPenerimaPage> createState() => _KonfirmasiPenerimaPageState();
|
||||
@ -34,11 +37,26 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
bool _setujuPenggunaan = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
// Controller untuk tanda tangan
|
||||
final SignatureController _signatureController = SignatureController(
|
||||
penStrokeWidth: 3,
|
||||
penColor: AppTheme.primaryColor,
|
||||
exportBackgroundColor: Colors.white,
|
||||
);
|
||||
|
||||
// Untuk menyimpan gambar tanda tangan
|
||||
Uint8List? _signatureImage;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Pastikan controller signature dibersihkan
|
||||
_signatureController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final warga = widget.penerima.warga;
|
||||
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
final timeFormat = DateFormat('HH:mm', 'id_ID');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@ -49,29 +67,40 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
body: Obx(
|
||||
() => controller.isProcessing.value || _isLoading
|
||||
? const Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildDetailPenerimaSection(warga),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailBantuanSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFotoBuktiSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildTandaTanganSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFormPersetujuanSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildKonfirmasiButton(),
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Sedang memproses konfirmasi...'),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailPenerimaSection(warga),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailBantuanSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFotoBuktiSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildTandaTanganSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildFormPersetujuanSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildKonfirmasiButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -158,7 +187,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
'Tempat, Tanggal Lahir',
|
||||
warga?['tempat_lahir'] != null &&
|
||||
warga?['tanggal_lahir'] != null
|
||||
? '${warga!['tempat_lahir']}, ${DateFormat('d MMMM yyyy').format(DateTime.parse(warga['tanggal_lahir']))}'
|
||||
? '${warga!['tempat_lahir']}, ${DateFormatter.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
|
||||
: 'Bogor, 2 Juni 1990'),
|
||||
const Divider(),
|
||||
|
||||
@ -183,9 +212,6 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
}
|
||||
|
||||
Widget _buildDetailBantuanSection() {
|
||||
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
final timeFormat = DateFormat('HH:mm', 'id_ID');
|
||||
|
||||
// Tentukan satuan berdasarkan data yang tersedia
|
||||
String satuan = '';
|
||||
if (widget.bentukBantuan?.satuan != null) {
|
||||
@ -197,10 +223,10 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
|
||||
String tanggalWaktuPenyaluran = '';
|
||||
if (widget.tanggalPenyaluran != null) {
|
||||
final tanggal = dateFormat.format(widget.tanggalPenyaluran!);
|
||||
final waktuMulai = timeFormat.format(widget.tanggalPenyaluran!);
|
||||
final waktuSelesai = timeFormat
|
||||
.format(widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
|
||||
final tanggal = DateFormatter.formatDate(widget.tanggalPenyaluran!);
|
||||
final waktuMulai = DateFormatter.formatTime(widget.tanggalPenyaluran!);
|
||||
final waktuSelesai = DateFormatter.formatTime(
|
||||
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
|
||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||
} else {
|
||||
tanggalWaktuPenyaluran = '09 April 2025 13:00-14:00';
|
||||
@ -328,39 +354,65 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Area tanda tangan
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 100,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Tanda Tangan Digital',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Image.asset(
|
||||
'assets/images/signature_placeholder.png',
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.network(
|
||||
'https://i.imgur.com/JMoZ0nR.png',
|
||||
height: 50,
|
||||
child: _signatureImage != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(
|
||||
_signatureImage!,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Signature(
|
||||
controller: _signatureController,
|
||||
backgroundColor: Colors.white,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Tombol aksi untuk tanda tangan
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Tombol hapus tanda tangan
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_signatureController.clear();
|
||||
_signatureImage = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: const Text('Hapus'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[100],
|
||||
foregroundColor: Colors.red[800],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tombol simpan tanda tangan
|
||||
ElevatedButton.icon(
|
||||
onPressed: _saveSignature,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Simpan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green[100],
|
||||
foregroundColor: Colors.green[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -531,6 +583,36 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSignature() async {
|
||||
if (_signatureController.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Tanda tangan tidak boleh kosong',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mendapatkan data tanda tangan
|
||||
final signature = await _signatureController.toPngBytes();
|
||||
|
||||
if (signature != null) {
|
||||
setState(() {
|
||||
_signatureImage = signature;
|
||||
});
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Tanda tangan berhasil disimpan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _konfirmasiPenerimaan() async {
|
||||
if (!_setujuPenerimaan || !_setujuPenggunaan) {
|
||||
Get.snackbar(
|
||||
@ -543,35 +625,98 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_signatureImage == null) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Tanda tangan digital diperlukan',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_buktiPenerimaan == null) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Foto bukti penerimaan diperlukan',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
String? imageUrl;
|
||||
Directory? tempDir;
|
||||
File? signatureFile;
|
||||
|
||||
if (_buktiPenerimaan != null) {
|
||||
// Upload bukti penerimaan
|
||||
imageUrl =
|
||||
await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
|
||||
}
|
||||
try {
|
||||
String imageUrl;
|
||||
String signatureUrl;
|
||||
|
||||
// Upload bukti penerimaan
|
||||
imageUrl = await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
|
||||
|
||||
// Simpan tanda tangan ke file sementara dan upload
|
||||
tempDir = await Directory.systemTemp.createTemp('signature');
|
||||
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,
|
||||
);
|
||||
|
||||
// Konfirmasi penerimaan
|
||||
await controller.konfirmasiPenerimaan(
|
||||
widget.penerima,
|
||||
buktiPenerimaan: imageUrl,
|
||||
tandaTangan: signatureUrl,
|
||||
);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Tutup semua snackbar yang mungkin masih terbuka
|
||||
if (Get.isSnackbarOpen) {
|
||||
Get.closeAllSnackbars();
|
||||
}
|
||||
|
||||
// Kembali ke halaman sebelumnya dengan hasil true (berhasil)
|
||||
// Gunakan Get.back(result: true) untuk kembali ke halaman detail penyaluran
|
||||
// dengan membawa hasil bahwa konfirmasi berhasil
|
||||
Get.back(result: true);
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Konfirmasi penerimaan bantuan berhasil',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
// Tampilkan snackbar sukses di halaman detail penyaluran
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Konfirmasi penerimaan bantuan berhasil',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
// Tampilkan pesan error
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan: $e',
|
||||
@ -580,6 +725,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
// Hapus file sementara jika belum dihapus
|
||||
try {
|
||||
if (signatureFile != null && signatureFile.existsSync()) {
|
||||
await signatureFile.delete();
|
||||
}
|
||||
if (tempDir != null && tempDir.existsSync()) {
|
||||
await tempDir.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error saat menghapus file sementara: $e');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = '';
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}),
|
||||
],
|
||||
|
@ -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:
|
||||
|
392
lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart
Normal file
392
lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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!,
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/profile/controllers/profile_controlle
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class ProfileView extends GetView<ProfileController> {
|
||||
const ProfileView({Key? key}) : super(key: key);
|
||||
const ProfileView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
Reference in New Issue
Block a user