Perbarui dependensi dan logika di JadwalSectionWidget serta hapus KonfirmasiPenerimaView. Modifikasi JadwalSectionWidget untuk menangani ID penyaluran dengan lebih baik dan menampilkan pesan kesalahan jika ID tidak ditemukan. Tambahkan rute baru untuk detail penyaluran dan perbarui rute aplikasi untuk mencerminkan perubahan ini.
This commit is contained in:
396
lib/app/modules/penyaluran/detail_penyaluran_controller.dart
Normal file
396
lib/app/modules/penyaluran/detail_penyaluran_controller.dart
Normal file
@ -0,0 +1,396 @@
|
||||
import 'package:get/get.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/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class DetailPenyaluranController extends GetxController {
|
||||
final SupabaseService _supabaseService = Get.find<SupabaseService>();
|
||||
|
||||
final isLoading = true.obs;
|
||||
final isProcessing = false.obs;
|
||||
final penyaluran = Rx<PenyaluranBantuanModel?>(null);
|
||||
final skemaBantuan = Rx<SkemaBantuanModel?>(null);
|
||||
final penerimaPenyaluran = <PenerimaPenyaluranModel>[].obs;
|
||||
|
||||
// Status untuk mengetahui apakah petugas desa
|
||||
final isPetugasDesa = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final String? penyaluranId = Get.parameters['id'];
|
||||
print('DetailPenyaluranController - ID Penyaluran: $penyaluranId');
|
||||
if (penyaluranId != null) {
|
||||
loadPenyaluranData(penyaluranId);
|
||||
checkUserRole();
|
||||
} else {
|
||||
isLoading.value = false;
|
||||
print('DetailPenyaluranController - ID Penyaluran tidak ditemukan');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkUserRole() async {
|
||||
try {
|
||||
final user = _supabaseService.client.auth.currentUser;
|
||||
if (user != null) {
|
||||
final userData = await _supabaseService.client
|
||||
.from('users')
|
||||
.select('role')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (userData != null && userData['role'] == 'petugas_desa') {
|
||||
isPetugasDesa.value = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error checking user role: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadPenyaluranData(String penyaluranId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
print(
|
||||
'DetailPenyaluranController - Memuat data penyaluran dengan ID: $penyaluranId');
|
||||
|
||||
// Ambil data penyaluran
|
||||
final penyaluranData = await _supabaseService.client
|
||||
.from('penyaluran_bantuan')
|
||||
.select('*')
|
||||
.eq('id', penyaluranId)
|
||||
.single();
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
print(
|
||||
'DetailPenyaluranController - Model penyaluran: ${penyaluran.value?.nama}');
|
||||
|
||||
// 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 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}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading penyaluran data: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memuat data penyaluran',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
if (penyaluran.value?.id != null) {
|
||||
await loadPenyaluranData(penyaluran.value!.id!);
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk memulai penyaluran bantuan
|
||||
Future<void> mulaiPenyaluran() async {
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (penyaluran.value?.id == null) {
|
||||
throw Exception('ID penyaluran tidak ditemukan');
|
||||
}
|
||||
|
||||
// Update status penyaluran menjadi "BERLANGSUNG"
|
||||
await _supabaseService.client
|
||||
.from('penyaluran_bantuan')
|
||||
.update({'status': 'BERLANGSUNG'}).eq('id', penyaluran.value!.id!);
|
||||
|
||||
await refreshData();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Penyaluran bantuan telah dimulai',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error memulai penyaluran: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat memulai penyaluran bantuan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk konfirmasi penerimaan bantuan oleh penerima
|
||||
Future<void> konfirmasiPenerimaan(PenerimaPenyaluranModel penerima,
|
||||
{String? buktiPenerimaan}) async {
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (penerima.id == null) {
|
||||
throw Exception('ID penerima tidak ditemukan');
|
||||
}
|
||||
|
||||
// Update status penerimaan menjadi "DITERIMA"
|
||||
final Map<String, dynamic> updateData = {
|
||||
'status_penerimaan': 'DITERIMA',
|
||||
'tanggal_penerimaan': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
if (buktiPenerimaan != null) {
|
||||
updateData['bukti_penerimaan'] = buktiPenerimaan;
|
||||
}
|
||||
|
||||
await _supabaseService.client
|
||||
.from('penerima_penyaluran')
|
||||
.update(updateData)
|
||||
.eq('id', penerima.id!);
|
||||
|
||||
await refreshData();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Konfirmasi penerimaan bantuan berhasil',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} 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,
|
||||
);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk menyelesaikan penyaluran bantuan
|
||||
Future<void> selesaikanPenyaluran() async {
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (penyaluran.value?.id == null) {
|
||||
throw Exception('ID penyaluran tidak ditemukan');
|
||||
}
|
||||
|
||||
// Cek apakah semua penerima sudah menerima bantuan
|
||||
final belumDiterima = penerimaPenyaluran
|
||||
.where((p) => p.statusPenerimaan?.toUpperCase() != 'DITERIMA')
|
||||
.toList();
|
||||
|
||||
if (belumDiterima.isNotEmpty) {
|
||||
final result = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('Konfirmasi'),
|
||||
content: Text(
|
||||
'Masih ada ${belumDiterima.length} penerima yang belum menerima bantuan. Apakah Anda yakin ingin menyelesaikan penyaluran?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('Ya, Selesaikan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != true) {
|
||||
isProcessing.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status penyaluran menjadi "TERLAKSANA"
|
||||
await _supabaseService.client.from('penyaluran_bantuan').update({
|
||||
'status': 'TERLAKSANA',
|
||||
'tanggal_selesai': DateTime.now().toIso8601String(),
|
||||
}).eq('id', penyaluran.value!.id!);
|
||||
|
||||
await refreshData();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Penyaluran bantuan telah diselesaikan',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error menyelesaikan penyaluran: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat menyelesaikan penyaluran bantuan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk membatalkan penyaluran bantuan
|
||||
Future<void> batalkanPenyaluran(String alasan) async {
|
||||
try {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (penyaluran.value?.id == null) {
|
||||
throw Exception('ID penyaluran tidak ditemukan');
|
||||
}
|
||||
|
||||
// Update status penyaluran menjadi "DIBATALKAN"
|
||||
await _supabaseService.client.from('penyaluran_bantuan').update({
|
||||
'status': 'DIBATALKAN',
|
||||
'alasan_pembatalan': alasan,
|
||||
'tanggal_selesai': DateTime.now().toIso8601String(),
|
||||
}).eq('id', penyaluran.value!.id!);
|
||||
|
||||
await refreshData();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Penyaluran bantuan telah dibatalkan',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error membatalkan penyaluran: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat membatalkan penyaluran bantuan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk mengupload bukti penerimaan
|
||||
Future<String?> uploadBuktiPenerimaan(String filePath) async {
|
||||
try {
|
||||
final fileName =
|
||||
'bukti_penerimaan_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final file = File(filePath);
|
||||
|
||||
final storageResponse = await _supabaseService.client.storage
|
||||
.from('bukti_penerimaan')
|
||||
.upload(fileName, file);
|
||||
|
||||
if (storageResponse.isEmpty) {
|
||||
throw Exception('Gagal mengupload bukti penerimaan');
|
||||
}
|
||||
|
||||
final fileUrl = _supabaseService.client.storage
|
||||
.from('bukti_penerimaan')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
return fileUrl;
|
||||
} catch (e) {
|
||||
print('Error upload bukti penerimaan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat mengupload bukti penerimaan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
765
lib/app/modules/penyaluran/detail_penyaluran_page.dart
Normal file
765
lib/app/modules/penyaluran/detail_penyaluran_page.dart
Normal file
@ -0,0 +1,765 @@
|
||||
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';
|
||||
|
||||
class DetailPenyaluranPage extends StatelessWidget {
|
||||
final controller = Get.put(DetailPenyaluranController());
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final searchController = TextEditingController();
|
||||
final RxString searchQuery = ''.obs;
|
||||
|
||||
DetailPenyaluranPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Detail Penyaluran'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.penyaluran.value == null) {
|
||||
return const Center(
|
||||
child: Text('Data penyaluran tidak ditemukan'),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.refreshData,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildPenerimaPenyaluranSection(context),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => _buildActionButtons(context)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header dengan status
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(penyaluran.status ?? '-'),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
|
||||
// Informasi penyaluran
|
||||
_buildInfoRow('Nama', penyaluran.nama ?? '-'),
|
||||
_buildInfoRow(
|
||||
'Tanggal',
|
||||
penyaluran.tanggalPenyaluran != null
|
||||
? dateFormat.format(penyaluran.tanggalPenyaluran!)
|
||||
: 'Belum dijadwalkan'),
|
||||
_buildInfoRow(
|
||||
'Jumlah Penerima', '${penyaluran.jumlahPenerima ?? 0} orang'),
|
||||
|
||||
// Informasi skema bantuan
|
||||
if (skema != null) ...[
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.category,
|
||||
size: 16, color: AppTheme.secondaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Skema: ${skema.nama ?? '-'}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
skema.deskripsi ?? 'Tidak ada deskripsi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Alasan penolakan jika ada
|
||||
if (penyaluran.alasanPenolakan != null &&
|
||||
penyaluran.alasanPenolakan!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Alasan Penolakan:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
penyaluran.alasanPenolakan!,
|
||||
style: TextStyle(
|
||||
color: Colors.red[700],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenerimaPenyaluranSection(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Daftar Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
Obx(() => Text(
|
||||
'${_getFilteredPenerima().length} Orang',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Search field
|
||||
TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari penerima...',
|
||||
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) {
|
||||
searchQuery.value = value.toLowerCase();
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Daftar penerima
|
||||
Obx(() {
|
||||
final filteredList = _getFilteredPenerima();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_off_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada data penerima',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredList.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildPenerimaItem(context, filteredList[index]);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<PenerimaPenyaluranModel> _getFilteredPenerima() {
|
||||
final query = searchQuery.value;
|
||||
if (query.isEmpty) {
|
||||
return controller.penerimaPenyaluran;
|
||||
}
|
||||
|
||||
return controller.penerimaPenyaluran.where((item) {
|
||||
final warga = item.warga;
|
||||
if (warga == null) return false;
|
||||
|
||||
final nama = warga['nama_lengkap']?.toString().toLowerCase() ?? '';
|
||||
final nik = warga['nik']?.toString().toLowerCase() ?? '';
|
||||
final alamat = warga['alamat']?.toString().toLowerCase() ?? '';
|
||||
final status = item.statusPenerimaan?.toLowerCase() ?? '';
|
||||
|
||||
return nama.contains(query) ||
|
||||
nik.contains(query) ||
|
||||
alamat.contains(query) ||
|
||||
status.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _buildPenerimaItem(
|
||||
BuildContext context, PenerimaPenyaluranModel item) {
|
||||
final warga = item.warga;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _showDetailPenerima(context, item),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: Text(
|
||||
warga != null && warga['nama_lengkap'] != null
|
||||
? warga['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Info penerima
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
warga != null
|
||||
? warga['nama_lengkap'] ?? 'Nama tidak tersedia'
|
||||
: 'Nama tidak tersedia',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'NIK: ${warga != null ? warga['nik'] ?? '-' : '-'}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status chip
|
||||
_buildStatusChip(item.statusPenerimaan ?? '-'),
|
||||
|
||||
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(String status) {
|
||||
Color backgroundColor;
|
||||
Color textColor = Colors.white;
|
||||
String statusText = _getStatusText(status);
|
||||
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU':
|
||||
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':
|
||||
backgroundColor = AppTheme.errorColor;
|
||||
break;
|
||||
default:
|
||||
backgroundColor = AppTheme.infoColor;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color backgroundColor;
|
||||
Color textColor = Colors.white;
|
||||
String statusText = _getStatusPenerimaanText(status);
|
||||
|
||||
// Konversi status ke format yang diinginkan
|
||||
if (status.toUpperCase() == 'SUDAHMENERIMA') {
|
||||
backgroundColor = AppTheme.successColor;
|
||||
statusText = 'Sudah Menerima';
|
||||
} else {
|
||||
// Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA
|
||||
backgroundColor = AppTheme.warningColor;
|
||||
statusText = 'Belum Menerima';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
final status = controller.penyaluran.value?.status?.toUpperCase() ?? '';
|
||||
|
||||
if (controller.isProcessing.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Jika status AKTIF, tampilkan tombol Selesaikan Penyaluran dan Batalkan
|
||||
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,
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Jika status TERLAKSANA atau DIBATALKAN, tidak perlu menampilkan tombol aksi
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showKonfirmasiPenerimaan(
|
||||
BuildContext context, PenerimaPenyaluranModel penerima) {
|
||||
// Dapatkan data jumlah bantuan dari penerima
|
||||
final jumlahBantuan = penerima.jumlahBantuan?.toString() ?? '5';
|
||||
|
||||
// Navigasi ke halaman konfirmasi penerima
|
||||
Get.to(
|
||||
() => KonfirmasiPenerimaPage(
|
||||
penerima: penerima,
|
||||
bentukBantuan:
|
||||
null, // Tidak ada data bentuk bantuan yang tersedia langsung
|
||||
jumlahBantuan: jumlahBantuan,
|
||||
tanggalPenyaluran: controller.penyaluran.value?.tanggalPenyaluran,
|
||||
),
|
||||
)?.then((result) {
|
||||
if (result == true) {
|
||||
// Refresh data jika konfirmasi berhasil
|
||||
controller.refreshData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showBatalkanDialog(BuildContext context) {
|
||||
final TextEditingController alasanController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Batalkan Penyaluran'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Masukkan alasan pembatalan penyaluran:'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: alasanController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Alasan pembatalan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (alasanController.text.trim().isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Alasan pembatalan tidak boleh kosong',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.batalkanPenyaluran(alasanController.text.trim());
|
||||
Get.back();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
child: const Text('Batalkan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetailPenerima(
|
||||
BuildContext context, PenerimaPenyaluranModel penerima) {
|
||||
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
final warga = penerima.warga;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Biodata Singkat',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const Divider(height: 30),
|
||||
if (warga != null) ...[
|
||||
_buildInfoRow('Nama', warga['nama_lengkap'] ?? '-'),
|
||||
_buildInfoRow('NIK', warga['nik'] ?? '-'),
|
||||
_buildInfoRow('Alamat Lengkap',
|
||||
'${warga['alamat'] ?? '-'} Desa ${warga['desa'] ?? '-'} Kecamatan ${warga['kecamatan'] ?? '-'} Kabupaten ${warga['kabupaten'] ?? '-'} Provinsi ${warga['provinsi'] ?? '-'}'),
|
||||
_buildInfoRow('Jenis Kelamin', warga['jenis_kelamin'] ?? '-'),
|
||||
_buildInfoRow('No. Telepon', warga['no_hp'] ?? '-'),
|
||||
],
|
||||
const Divider(height: 30),
|
||||
_buildInfoRow('Status Penerimaan',
|
||||
_getStatusPenerimaanText(penerima.statusPenerimaan ?? '-')),
|
||||
if (penerima.tanggalPenerimaan != null)
|
||||
_buildInfoRow('Tanggal Penerimaan',
|
||||
dateFormat.format(penerima.tanggalPenerimaan!)),
|
||||
if (penerima.jumlahBantuan != null)
|
||||
_buildInfoRow(
|
||||
'Jumlah Bantuan', penerima.jumlahBantuan.toString()),
|
||||
if (penerima.keterangan != null &&
|
||||
penerima.keterangan!.isNotEmpty)
|
||||
_buildInfoRow('Keterangan', penerima.keterangan!),
|
||||
if (penerima.buktiPenerimaan != null &&
|
||||
penerima.buktiPenerimaan!.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Bukti Penerimaan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
penerima.buktiPenerimaan!,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
color: Colors.grey[300],
|
||||
child: const Center(
|
||||
child: Text('Gagal memuat gambar'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 30),
|
||||
if (controller.penyaluran.value?.status?.toUpperCase() ==
|
||||
'AKTIF' &&
|
||||
penerima.statusPenerimaan?.toUpperCase() !=
|
||||
'SUDAHMENERIMA') ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text(
|
||||
'Konfirmasi Penerimaan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showKonfirmasiPenerimaan(context, penerima);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Tutup',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status.toUpperCase()) {
|
||||
case 'MENUNGGU':
|
||||
return 'Menunggu Persetujuan';
|
||||
case 'DISETUJUI':
|
||||
return 'Disetujui';
|
||||
case 'DITOLAK':
|
||||
return 'Ditolak';
|
||||
case 'AKTIF':
|
||||
return 'Sedang AKTIF';
|
||||
case 'TERLAKSANA':
|
||||
return 'Terlaksana';
|
||||
case 'DIBATALKAN':
|
||||
return 'Dibatalkan';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusPenerimaanText(String status) {
|
||||
// Konversi status ke format yang diinginkan
|
||||
if (status.toUpperCase() == 'SUDAHMENERIMA') {
|
||||
return 'Sudah Menerima';
|
||||
} else {
|
||||
// Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA
|
||||
return 'Belum Menerima';
|
||||
}
|
||||
}
|
||||
}
|
588
lib/app/modules/penyaluran/konfirmasi_penerima_page.dart
Normal file
588
lib/app/modules/penyaluran/konfirmasi_penerima_page.dart
Normal file
@ -0,0 +1,588 @@
|
||||
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 'dart:io';
|
||||
|
||||
class KonfirmasiPenerimaPage extends StatefulWidget {
|
||||
final PenerimaPenyaluranModel penerima;
|
||||
final BentukBantuanModel? bentukBantuan;
|
||||
final String? jumlahBantuan;
|
||||
final DateTime? tanggalPenyaluran;
|
||||
|
||||
const KonfirmasiPenerimaPage({
|
||||
Key? key,
|
||||
required this.penerima,
|
||||
this.bentukBantuan,
|
||||
this.jumlahBantuan,
|
||||
this.tanggalPenyaluran,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<KonfirmasiPenerimaPage> createState() => _KonfirmasiPenerimaPageState();
|
||||
}
|
||||
|
||||
class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
final controller = Get.find<DetailPenyaluranController>();
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
File? _buktiPenerimaan;
|
||||
bool _setujuPenerimaan = false;
|
||||
bool _setujuPenggunaan = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
@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(
|
||||
title: const Text('Form Konfirmasi Penerimaan Bantuan'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: 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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailPenerimaSection(Map<String, dynamic>? warga) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Detail Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Foto Identitas
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Foto Identitas',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 60,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: warga?['foto_identitas'] != null
|
||||
? DecorationImage(
|
||||
image: NetworkImage(warga!['foto_identitas']),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: warga?['foto_identitas'] == null
|
||||
? const Icon(Icons.person, color: Colors.grey)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// NIK
|
||||
_buildInfoRow('NIK', warga?['nik'] ?? '3201020107030010'),
|
||||
const Divider(),
|
||||
|
||||
// No KK
|
||||
_buildInfoRow('No KK', warga?['no_kk'] ?? '3201020107030393'),
|
||||
const Divider(),
|
||||
|
||||
// No Handphone
|
||||
_buildInfoRow(
|
||||
'No Handphone', warga?['no_telepon'] ?? '089891256532'),
|
||||
const Divider(),
|
||||
|
||||
// Email
|
||||
_buildInfoRow('Email', warga?['email'] ?? 'bajiyadi@gmail.com'),
|
||||
const Divider(),
|
||||
|
||||
// Jenis Kelamin
|
||||
_buildInfoRow('Jenis Kelamin', warga?['jenis_kelamin'] ?? 'Pria'),
|
||||
const Divider(),
|
||||
|
||||
// Agama
|
||||
_buildInfoRow('Agama', warga?['agama'] ?? 'Islam'),
|
||||
const Divider(),
|
||||
|
||||
// Tempat, Tanggal Lahir
|
||||
_buildInfoRow(
|
||||
'Tempat, Tanggal Lahir',
|
||||
warga?['tempat_lahir'] != null &&
|
||||
warga?['tanggal_lahir'] != null
|
||||
? '${warga!['tempat_lahir']}, ${DateFormat('d MMMM yyyy').format(DateTime.parse(warga['tanggal_lahir']))}'
|
||||
: 'Bogor, 2 Juni 1990'),
|
||||
const Divider(),
|
||||
|
||||
// Alamat Lengkap
|
||||
_buildInfoRow(
|
||||
'Alamat Lengkap',
|
||||
warga?['alamat'] ??
|
||||
'Jl. Letda Natsir No. 22 RT 001/003\nKec. Gunung Putri Kab. Bogor'),
|
||||
const Divider(),
|
||||
|
||||
// Pekerjaan
|
||||
_buildInfoRow('Pekerjaan', warga?['pekerjaan'] ?? 'Petani'),
|
||||
const Divider(),
|
||||
|
||||
// Pendidikan Terakhir
|
||||
_buildInfoRow('Pendidikan Terakhir',
|
||||
warga?['pendidikan_terakhir'] ?? 'Sekolah Dasar (SD)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
satuan = widget.bentukBantuan!.satuan!;
|
||||
} else {
|
||||
// Default satuan jika tidak ada
|
||||
satuan = 'Kg';
|
||||
}
|
||||
|
||||
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)));
|
||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||
} else {
|
||||
tanggalWaktuPenyaluran = '09 April 2025 13:00-14:00';
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Detail Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Bentuk Bantuan
|
||||
_buildInfoRow(
|
||||
'Bentuk Bantuan', widget.bentukBantuan?.nama ?? 'Beras'),
|
||||
const Divider(),
|
||||
|
||||
// Nilai Bantuan
|
||||
_buildInfoRow(
|
||||
'Nilai Bantuan', '${widget.jumlahBantuan ?? '5'}$satuan'),
|
||||
const Divider(),
|
||||
|
||||
// Tanggal Penyaluran
|
||||
_buildInfoRow('Tanggal Penyaluran', tanggalWaktuPenyaluran),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFotoBuktiSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Foto Bukti Penerimaan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: _ambilFoto,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: _buktiPenerimaan != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.file(
|
||||
_buktiPenerimaan!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_photo_alternate_outlined,
|
||||
size: 40,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tambah Foto',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTandaTanganSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Tanda Tangan Digital Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
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,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormPersetujuanSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Form Persetujuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Checkbox persetujuan 1
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(
|
||||
value: _setujuPenerimaan,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_setujuPenerimaan = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Saya telah menerima bantuan dengan jumlah dan kondisi yang sesuai.',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Checkbox persetujuan 2
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(
|
||||
value: _setujuPenggunaan,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_setujuPenggunaan = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Saya akan menggunakan bantuan dengan sebaik-baiknya',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKonfirmasiButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _setujuPenerimaan && _setujuPenggunaan
|
||||
? _konfirmasiPenerimaan
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
disabledBackgroundColor: Colors.grey[300],
|
||||
disabledForegroundColor: Colors.grey[600],
|
||||
),
|
||||
child: const Text(
|
||||
'Konfirmasi Penerimaan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _ambilFoto() async {
|
||||
try {
|
||||
final XFile? image = await _picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
setState(() {
|
||||
_buktiPenerimaan = File(image.path);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengambil foto: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _konfirmasiPenerimaan() async {
|
||||
if (!_setujuPenerimaan || !_setujuPenggunaan) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Anda harus menyetujui semua persyaratan',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
String? imageUrl;
|
||||
|
||||
if (_buktiPenerimaan != null) {
|
||||
// Upload bukti penerimaan
|
||||
imageUrl =
|
||||
await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
|
||||
}
|
||||
|
||||
// Konfirmasi penerimaan
|
||||
await controller.konfirmasiPenerimaan(
|
||||
widget.penerima,
|
||||
buktiPenerimaan: imageUrl,
|
||||
);
|
||||
|
||||
Get.back(result: true);
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Konfirmasi penerimaan bantuan berhasil',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
11
lib/app/modules/penyaluran/penyaluran_binding.dart
Normal file
11
lib/app/modules/penyaluran/penyaluran_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/detail_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
|
||||
class PenyaluranBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<SupabaseService>(() => SupabaseService());
|
||||
Get.lazyPut<DetailPenyaluranController>(() => DetailPenyaluranController());
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ 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_time_helper.dart';
|
||||
|
||||
class JadwalSectionWidget extends StatelessWidget {
|
||||
@ -203,8 +202,16 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
// Hanya kirim ID penyaluran
|
||||
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwal.id);
|
||||
if (jadwal.id != null) {
|
||||
Get.toNamed(Routes.detailPenyaluran,
|
||||
parameters: {'id': jadwal.id!});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID penyaluran tidak ditemukan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -314,23 +321,30 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
textTheme,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
// Hanya kirim ID penyaluran
|
||||
Get.toNamed(Routes.pelaksanaanPenyaluran,
|
||||
arguments: jadwal.id);
|
||||
},
|
||||
icon: const Icon(Icons.info_outline, size: 16),
|
||||
label: const Text('Lihat Detail'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppTheme.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
if (jadwal.id != null) {
|
||||
Get.toNamed(Routes.detailPenyaluran,
|
||||
parameters: {'id': jadwal.id!});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID penyaluran tidak ditemukan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.visibility_outlined),
|
||||
label: const Text('Lihat Detail'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: statusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,669 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pelaksanaan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class KonfirmasiPenerimaView extends GetView<PelaksanaanPenyaluranController> {
|
||||
const KonfirmasiPenerimaView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Ambil data dari arguments
|
||||
final Map<String, dynamic> args = Get.arguments ?? {};
|
||||
|
||||
// Pastikan semua parameter yang diperlukan tersedia
|
||||
final penerimaId = args['penerima_id'] ?? 0;
|
||||
final String penyaluranId = args['penyaluran_id']?.toString() ?? '';
|
||||
final Map<String, dynamic> warga =
|
||||
args['warga'] as Map<String, dynamic>? ?? {};
|
||||
final Map<String, dynamic> jadwal =
|
||||
args['jadwal'] as Map<String, dynamic>? ?? {};
|
||||
final String statusPenerimaan =
|
||||
args['status_penerimaan']?.toString() ?? 'BELUMMENERIMA';
|
||||
final dynamic jumlahBantuan = args['jumlah_bantuan'] ?? 1;
|
||||
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Konfirmasi Penerima'),
|
||||
),
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Konfirmasi Penerima'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header dengan foto dan nama
|
||||
_buildHeader(warga),
|
||||
|
||||
// Detail informasi penerima
|
||||
_buildDetailInfo(warga),
|
||||
|
||||
// Detail jadwal dan bantuan
|
||||
_buildDetailJadwalBantuan(jadwal, jumlahBantuan),
|
||||
|
||||
// Form konfirmasi
|
||||
_buildKonfirmasiForm(context, penerimaId, penyaluranId),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar:
|
||||
_buildBottomButtons(penerimaId, penyaluranId, statusPenerimaan),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildHeader(Map<String, dynamic> warga) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Foto profil
|
||||
CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white,
|
||||
child: warga['foto_url'] != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
child: Image.network(
|
||||
warga['foto_url'],
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Nama penerima
|
||||
Text(
|
||||
warga['nama'] ?? 'Nama tidak tersedia',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// NIK
|
||||
Text(
|
||||
warga['nik'] ?? 'NIK tidak tersedia',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Badge status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: controller.getStatusColor(
|
||||
warga['status_penerimaan'] ?? 'BELUMMENERIMA'),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
controller.getStatusIcon(
|
||||
warga['status_penerimaan'] ?? 'BELUMMENERIMA'),
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.getStatusText(
|
||||
warga['status_penerimaan'] ?? 'BELUMMENERIMA'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailInfo(Map<String, dynamic> warga) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Detail Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow('NIK', warga['nik'] ?? '-'),
|
||||
_buildDetailRow('No KK', warga['no_kk'] ?? '-'),
|
||||
_buildDetailRow('No Handphone', warga['no_hp'] ?? '-'),
|
||||
_buildDetailRow('Email', warga['email'] ?? '-'),
|
||||
_buildDetailRow(
|
||||
'Jenis Kelamin', warga['jenis_kelamin'] ?? '-'),
|
||||
_buildDetailRow('Agama', warga['agama'] ?? '-'),
|
||||
_buildDetailRow('Tempat, Tanggal Lahir',
|
||||
'${warga['tempat_lahir'] ?? '-'}, ${warga['tanggal_lahir'] ?? '-'}'),
|
||||
_buildDetailRow('Alamat Lengkap', warga['alamat'] ?? '-'),
|
||||
_buildDetailRow('Pekerjaan', warga['pekerjaan'] ?? '-'),
|
||||
_buildDetailRow(
|
||||
'Pendidikan Terakhir', warga['pendidikan'] ?? '-'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailJadwalBantuan(
|
||||
Map<String, dynamic> jadwal, dynamic jumlahBantuan) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Detail Jadwal & Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
'Tanggal Penyaluran', jadwal['tanggal'] ?? '-'),
|
||||
_buildDetailRow('Waktu', jadwal['waktu'] ?? '-'),
|
||||
_buildDetailRow('Lokasi', jadwal['lokasi'] ?? '-'),
|
||||
_buildDetailRow(
|
||||
'Jenis Bantuan', jadwal['jenis_bantuan'] ?? '-'),
|
||||
_buildDetailRow('Jumlah Bantuan', '$jumlahBantuan item'),
|
||||
_buildDetailRow('Keterangan', jadwal['keterangan'] ?? '-'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKonfirmasiForm(
|
||||
BuildContext context, int penerimaId, String penyaluranId) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Konfirmasi Penyaluran Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status penyaluran
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.infoColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.infoColor.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.info_outline,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Pastikan penerima hadir dan menerima bantuan sesuai dengan ketentuan.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Checkbox persetujuan petugas
|
||||
const Text(
|
||||
'Persetujuan Petugas',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Checkbox 1
|
||||
Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: controller.isKonfirmasiChecked.value,
|
||||
onChanged: (value) {
|
||||
controller.isKonfirmasiChecked.value =
|
||||
value ?? false;
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
)),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Saya konfirmasi bahwa penerima ini telah hadir dan menerima bantuan sesuai dengan ketentuan',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Checkbox 2
|
||||
Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: controller.isIdentitasChecked.value,
|
||||
onChanged: (value) {
|
||||
controller.isIdentitasChecked.value =
|
||||
value ?? false;
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
)),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Saya telah memverifikasi identitas penerima sesuai dengan KTP/KK yang ditunjukkan',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Checkbox 3
|
||||
Row(
|
||||
children: [
|
||||
Obx(() => Checkbox(
|
||||
value: controller.isDataValidChecked.value,
|
||||
onChanged: (value) {
|
||||
controller.isDataValidChecked.value =
|
||||
value ?? false;
|
||||
},
|
||||
activeColor: AppTheme.primaryColor,
|
||||
)),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Saya menyatakan bahwa data yang diinput adalah benar dan dapat dipertanggungjawabkan',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Form bukti foto
|
||||
const Text(
|
||||
'Bukti Foto Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => controller.pilihFotoBukti(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: Colors.grey.shade300, width: 1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Obx(() => controller.fotoBuktiPath.value.isEmpty
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.camera_alt,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Tambahkan Foto Bukti',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format: JPG, PNG (Maks. 5MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.fotoBuktiPath.value,
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: InkWell(
|
||||
onTap: () => controller.hapusFotoBukti(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Tanda tangan digital penerima
|
||||
const Text(
|
||||
'Tanda Tangan Digital Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => controller.bukaSignaturePad(context),
|
||||
child: Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Obx(() => controller.tandaTanganPath.value.isEmpty
|
||||
? const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.draw,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 40,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap untuk menambahkan tanda tangan',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.tandaTanganPath.value,
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
size: 40,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: InkWell(
|
||||
onTap: () => controller.hapusTandaTangan(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Form catatan
|
||||
const Text(
|
||||
'Catatan Penyaluran (Opsional)',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller.catatanController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan catatan penyaluran jika ada',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomButtons(
|
||||
int penerimaId, String penyaluranId, String statusPenerimaan) {
|
||||
final bool sudahDiterima = statusPenerimaan == 'SUDAHMENERIMA';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, -3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Kembali'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton(
|
||||
onPressed: sudahDiterima
|
||||
? null
|
||||
: (controller.isKonfirmasiChecked.value &&
|
||||
controller.isIdentitasChecked.value &&
|
||||
controller.isDataValidChecked.value &&
|
||||
controller.fotoBuktiPath.value.isNotEmpty &&
|
||||
controller.tandaTanganPath.value.isNotEmpty
|
||||
? () => controller.konfirmasiPenyaluran(
|
||||
penerimaId, penyaluranId)
|
||||
: null),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
disabledBackgroundColor: Colors.grey.shade300,
|
||||
),
|
||||
child:
|
||||
Text(sudahDiterima ? 'Sudah Dikonfirmasi' : 'Konfirmasi'),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,11 +6,12 @@ import 'package:penyaluran_app/app/modules/petugas_desa/bindings/petugas_desa_bi
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_penerima_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penerima_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penitipan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/detail_penyaluran_page.dart';
|
||||
import 'package:penyaluran_app/app/modules/penyaluran/penyaluran_binding.dart';
|
||||
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart';
|
||||
@ -57,11 +58,6 @@ class AppPages {
|
||||
page: () => const DetailPenerimaView(),
|
||||
binding: PenerimaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.konfirmasiPenerima,
|
||||
page: () => const KonfirmasiPenerimaView(),
|
||||
binding: PenerimaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.profile,
|
||||
page: () => const ProfileView(),
|
||||
@ -87,5 +83,10 @@ class AppPages {
|
||||
page: () => const TambahPenyaluranView(),
|
||||
binding: PetugasDesaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.detailPenyaluran,
|
||||
page: () => DetailPenyaluranPage(),
|
||||
binding: PenyaluranBinding(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ abstract class Routes {
|
||||
static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran;
|
||||
static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran;
|
||||
static const laporanPenyaluran = _Paths.laporanPenyaluran;
|
||||
static const detailPenyaluran = _Paths.detailPenyaluran;
|
||||
}
|
||||
|
||||
abstract class _Paths {
|
||||
@ -48,4 +49,5 @@ abstract class _Paths {
|
||||
static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran';
|
||||
static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran';
|
||||
static const laporanPenyaluran = '/laporan-penyaluran';
|
||||
static const detailPenyaluran = '/detail-penyaluran';
|
||||
}
|
||||
|
@ -492,7 +492,7 @@ class SupabaseService extends GetxService {
|
||||
final fileKey =
|
||||
'$folder/${DateTime.now().millisecondsSinceEpoch}.$fileExt';
|
||||
|
||||
final file = await client.storage.from(bucket).upload(
|
||||
await client.storage.from(bucket).upload(
|
||||
fileKey,
|
||||
File(filePath),
|
||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true),
|
||||
|
17
lib/app/theme/app_colors.dart
Normal file
17
lib/app/theme/app_colors.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
static Color primary = const Color(0xFF2E7D32); // Green 800
|
||||
static Color secondary = const Color(0xFF1565C0); // Blue 800
|
||||
static Color accent = const Color(0xFFFFA000); // Amber 700
|
||||
static Color background = const Color(0xFFF5F5F5); // Grey 100
|
||||
static Color surface = Colors.white;
|
||||
static Color error = const Color(0xFFD32F2F); // Red 700
|
||||
static Color success = const Color(0xFF388E3C); // Green 700
|
||||
static Color warning = const Color(0xFFFFA000); // Amber 700
|
||||
static Color info = const Color(0xFF1976D2); // Blue 700
|
||||
static Color textPrimary = const Color(0xFF212121); // Grey 900
|
||||
static Color textSecondary = const Color(0xFF757575); // Grey 600
|
||||
static Color divider = const Color(0xFFBDBDBD); // Grey 400
|
||||
static Color disabled = const Color(0xFFE0E0E0); // Grey 300
|
||||
}
|
54
lib/app/widgets/custom_app_bar.dart
Normal file
54
lib/app/widgets/custom_app_bar.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final bool showBackButton;
|
||||
final List<Widget>? actions;
|
||||
final Widget? leading;
|
||||
final bool centerTitle;
|
||||
final double elevation;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
|
||||
const CustomAppBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.showBackButton = false,
|
||||
this.actions,
|
||||
this.leading,
|
||||
this.centerTitle = true,
|
||||
this.elevation = 0,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: foregroundColor ?? Colors.white,
|
||||
),
|
||||
),
|
||||
centerTitle: centerTitle,
|
||||
elevation: elevation,
|
||||
backgroundColor: backgroundColor ?? AppTheme.primaryColor,
|
||||
foregroundColor: foregroundColor ?? Colors.white,
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Get.back(),
|
||||
)
|
||||
: leading,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
48
lib/app/widgets/loading_indicator.dart
Normal file
48
lib/app/widgets/loading_indicator.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_colors.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final String? message;
|
||||
final Color? color;
|
||||
final double size;
|
||||
|
||||
const LoadingIndicator({
|
||||
Key? key,
|
||||
this.message,
|
||||
this.color,
|
||||
this.size = 40.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
color ?? AppColors.primary,
|
||||
),
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user