Files
penyaluran_app/lib/app/modules/penyaluran/detail_penyaluran_page.dart

766 lines
25 KiB
Dart

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';
}
}
}