Perbarui dependensi dan tambahkan fungsionalitas laporan penyaluran. Tambahkan paket baru seperti file_picker, pdf, dan open_file ke dalam pubspec.yaml. Hapus model LaporanModel yang tidak digunakan dan ganti dengan LaporanPenyaluranModel. Modifikasi tampilan dan controller untuk mendukung pengelolaan laporan penyaluran, termasuk navigasi dan ekspor ke PDF. Perbarui rute aplikasi untuk mencakup halaman laporan penyaluran baru.
This commit is contained in:
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
|
||||
class LaporanPenyaluranBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<LaporanPenyaluranController>(
|
||||
() => LaporanPenyaluranController(),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,623 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranCreateView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final penyaluranId = Get.arguments as String;
|
||||
|
||||
// Dapatkan info penyaluran
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchPenyaluranDetail(penyaluranId);
|
||||
controller.resetForm(); // Reset form setiap kali halaman dibuka
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Buat Laporan Penyaluran',
|
||||
// subtitle: 'Isi form untuk membuat laporan penyaluran',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informasi penyaluran
|
||||
if (controller.selectedPenyaluran.value != null) ...[
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Informasi Penyaluran',
|
||||
// subtitle: 'Penyaluran yang akan dibuatkan laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(
|
||||
'Nama Penyaluran',
|
||||
controller.selectedPenyaluran.value!.nama ?? '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Penyaluran',
|
||||
controller.selectedPenyaluran.value!
|
||||
.tanggalPenyaluran !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
.selectedPenyaluran.value!.tanggalPenyaluran!)
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Selesai',
|
||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
.selectedPenyaluran.value!.tanggalSelesai!)
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Jumlah Penerima',
|
||||
'${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Form laporan
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Form Laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Judul laporan
|
||||
_buildTextField(
|
||||
controller: controller.judulController,
|
||||
label: 'Judul Laporan',
|
||||
hint: 'Masukkan judul laporan',
|
||||
required: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Dokumentasi dan Berita Acara
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Dokumentasi & Berita Acara',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Upload Dokumentasi
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => controller.dokumentasiPath.isNotEmpty
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
File(controller
|
||||
.dokumentasiPath.value),
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller
|
||||
.dokumentasiPath.value = '';
|
||||
},
|
||||
icon: Icon(Icons.delete,
|
||||
color: AppTheme.errorColor),
|
||||
label: Text('Hapus',
|
||||
style: TextStyle(
|
||||
color:
|
||||
AppTheme.errorColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label:
|
||||
const Text('Upload Foto Dokumentasi'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format gambar: JPG, PNG, JPEG (maks. 5MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Upload Berita Acara
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Berita Acara',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => controller.beritaAcaraPath.isNotEmpty
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor
|
||||
.withOpacity(0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.description,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dokumen Berita Acara',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
controller.beritaAcaraPath.value
|
||||
.split('/')
|
||||
.last,
|
||||
style: const TextStyle(
|
||||
fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.beritaAcaraPath.value =
|
||||
'';
|
||||
},
|
||||
icon: Icon(Icons.delete,
|
||||
color: AppTheme.errorColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.file_present),
|
||||
label: const Text(
|
||||
'Upload Dokumen Berita Acara'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format file: PDF, DOC, DOCX (maks. 10MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Tombol simpan
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isSaving.value
|
||||
? null
|
||||
: () {
|
||||
if (controller.judulController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Judul laporan wajib diisi',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
controller.saveLaporan(penyaluranId);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: controller.isSaving.value
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Simpan Laporan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk memilih gambar dokumentasi
|
||||
void _pickDocumentationImage(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
context,
|
||||
Icons.camera_alt,
|
||||
'Kamera',
|
||||
ImageSource.camera,
|
||||
Colors.blue,
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
context,
|
||||
Icons.photo_library,
|
||||
'Galeri',
|
||||
ImageSource.gallery,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk opsi sumber gambar
|
||||
Widget _buildImageSourceOption(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
ImageSource source,
|
||||
Color color,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final ImagePicker picker = ImagePicker();
|
||||
try {
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: source,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (image != null) {
|
||||
controller.dokumentasiPath.value = image.path;
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih gambar: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 48, color: color),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk memilih file berita acara
|
||||
Future<void> _pickBeritaAcaraFile(BuildContext context) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'doc', 'docx'],
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
controller.beritaAcaraPath.value = result.files.single.path!;
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih file: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget untuk item informasi
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk input teks
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
bool required = false,
|
||||
int maxLines = 1,
|
||||
bool isReadOnly = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
children: required
|
||||
? [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(
|
||||
color: AppTheme.errorColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
readOnly: isReadOnly,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: AppTheme.primaryColor, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,700 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class LaporanPenyaluranEditView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranEditView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final laporanId = Get.arguments as String;
|
||||
|
||||
// Dapatkan data laporan
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchLaporanDetail(laporanId).then((_) {
|
||||
if (controller.selectedLaporan.value != null) {
|
||||
controller.setFormForEdit(controller.selectedLaporan.value!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Edit Laporan Penyaluran',
|
||||
// subtitle: 'Perbarui informasi laporan penyaluran',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.selectedLaporan.value == null) {
|
||||
return const Center(
|
||||
child: Text('Laporan tidak ditemukan'),
|
||||
);
|
||||
}
|
||||
|
||||
// Cek status laporan
|
||||
if (controller.selectedLaporan.value!.status == 'FINAL') {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lock,
|
||||
size: 64,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Laporan Telah Difinalisasi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Laporan yang sudah difinalisasi tidak dapat diedit lagi.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Kembali ke Detail Laporan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informasi penyaluran
|
||||
if (controller.selectedPenyaluran.value != null) ...[
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Informasi Penyaluran',
|
||||
// subtitle: 'Penyaluran yang terkait dengan laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(
|
||||
'Nama Penyaluran',
|
||||
controller.selectedPenyaluran.value!.nama ?? '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Penyaluran',
|
||||
controller.selectedPenyaluran.value!
|
||||
.tanggalPenyaluran !=
|
||||
null
|
||||
? '${controller.selectedPenyaluran.value!.tanggalPenyaluran!.day}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.month}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.year}'
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Selesai',
|
||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||
null
|
||||
? '${controller.selectedPenyaluran.value!.tanggalSelesai!.day}/${controller.selectedPenyaluran.value!.tanggalSelesai!.month}/${controller.selectedPenyaluran.value!.tanggalSelesai!.year}'
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Jumlah Penerima',
|
||||
'${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Form laporan
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Form Laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Judul laporan
|
||||
_buildTextField(
|
||||
controller: controller.judulController,
|
||||
label: 'Judul Laporan',
|
||||
hint: 'Masukkan judul laporan',
|
||||
required: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Dokumentasi dan Berita Acara
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Dokumentasi & Berita Acara',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Upload Dokumentasi
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Dokumentasi yang sudah ada
|
||||
if (controller
|
||||
.selectedLaporan.value?.dokumentasiUrl !=
|
||||
null &&
|
||||
controller.dokumentasiPath.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.selectedLaporan.value!
|
||||
.dokumentasiUrl!,
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child:
|
||||
Text('Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.edit,
|
||||
color: Colors.blue),
|
||||
label: const Text('Ganti',
|
||||
style:
|
||||
TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Dokumentasi yang baru dipilih
|
||||
else if (controller.dokumentasiPath.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
File(controller.dokumentasiPath.value),
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child:
|
||||
Text('Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.dokumentasiPath.value = '';
|
||||
},
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
label: const Text('Hapus',
|
||||
style:
|
||||
TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Tidak ada dokumentasi
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Upload Foto Dokumentasi'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(color: Colors.blue.shade300),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format gambar: JPG, PNG, JPEG (maks. 5MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Upload Berita Acara
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Berita Acara',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Berita acara yang sudah ada
|
||||
if (controller
|
||||
.selectedLaporan.value?.beritaAcaraUrl !=
|
||||
null &&
|
||||
controller.beritaAcaraPath.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.description,
|
||||
size: 40,
|
||||
color: Colors.blue.shade700),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dokumen Berita Acara',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
Uri.parse(controller
|
||||
.selectedLaporan
|
||||
.value!
|
||||
.beritaAcaraUrl!)
|
||||
.pathSegments
|
||||
.last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.edit,
|
||||
color: Colors.blue),
|
||||
label: const Text('Ganti',
|
||||
style:
|
||||
TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Berita acara yang baru dipilih
|
||||
else if (controller.beritaAcaraPath.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.description,
|
||||
size: 40,
|
||||
color: Colors.blue.shade700),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
File(controller
|
||||
.beritaAcaraPath.value)
|
||||
.path
|
||||
.split('/')
|
||||
.last,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'Dokumen berita acara',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.beritaAcaraPath.value = '';
|
||||
},
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
label: const Text('Hapus',
|
||||
style:
|
||||
TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Tidak ada berita acara
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Upload Dokumen Berita Acara'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(color: Colors.blue.shade300),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format dokumen: PDF, DOC, DOCX (maks. 10MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton(
|
||||
onPressed: controller.isSaving.value
|
||||
? null
|
||||
: () => controller.updateLaporan(laporanId),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: controller.isSaving.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child:
|
||||
CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
'Simpan Perubahan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk item informasi
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text(' : '),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk input teks
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
int maxLines = 1,
|
||||
bool required = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
children: required
|
||||
? const [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
maxLines: maxLines,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk memilih gambar dokumentasi
|
||||
Future<void> _pickDocumentationImage(BuildContext context) async {
|
||||
try {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
controller.dokumentasiPath.value = image.path;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Terjadi kesalahan: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk memilih file berita acara
|
||||
Future<void> _pickBeritaAcaraFile(BuildContext context) async {
|
||||
try {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'doc', 'docx'],
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
controller.beritaAcaraPath.value = result.files.single.path!;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Terjadi kesalahan: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,410 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Laporan Penyaluran Bantuan',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Filter status
|
||||
_buildStatusFilter(),
|
||||
|
||||
// Daftar laporan
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.daftarLaporan.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.note_alt_outlined,
|
||||
size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum ada laporan penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Buat laporan baru untuk penyaluran yang telah selesai',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showPenyaluranDialog(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Buat Laporan Baru'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter berdasarkan status jika dipilih
|
||||
final filteredLaporan = controller.filterStatus.value == 'SEMUA'
|
||||
? controller.daftarLaporan
|
||||
: controller.daftarLaporan
|
||||
.where((laporan) =>
|
||||
laporan.status == controller.filterStatus.value)
|
||||
.toList();
|
||||
|
||||
if (filteredLaporan.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Tidak ada laporan dengan status ${controller.filterStatus.value}'),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchLaporan();
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: filteredLaporan.length,
|
||||
itemBuilder: (context, index) {
|
||||
final laporan = filteredLaporan[index];
|
||||
return _buildLaporanCard(context, laporan);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showPenyaluranDialog(context),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk filter status
|
||||
Widget _buildStatusFilter() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
color: AppTheme.primaryColor.withOpacity(0.05),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Filter Status',
|
||||
// subtitle: 'Tampilkan laporan berdasarkan status',
|
||||
// showDivider: false,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip('SEMUA'),
|
||||
_buildFilterChip('DRAFT'),
|
||||
_buildFilterChip('FINAL'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Chip untuk filter
|
||||
Widget _buildFilterChip(String status) {
|
||||
return Obx(() {
|
||||
final isSelected = controller.filterStatus.value == status;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(status),
|
||||
onSelected: (_) {
|
||||
controller.filterStatus.value = status;
|
||||
},
|
||||
backgroundColor: Colors.white,
|
||||
checkmarkColor: Colors.white,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Widget untuk card laporan
|
||||
Widget _buildLaporanCard(
|
||||
BuildContext context, LaporanPenyaluranModel laporan) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.id);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
laporan.judul,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
StatusBadge(status: laporan.status ?? 'DRAFT'),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24, color: Colors.white30),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Tanggal: ${laporan.tanggalLaporan != null ? DateTimeHelper.formatDateTime(laporan.tanggalLaporan!) : '-'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (laporan.status == 'FINAL')
|
||||
Material(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
controller
|
||||
.fetchPenyaluranDetail(
|
||||
laporan.penyaluranBantuanId)
|
||||
.then((_) {
|
||||
if (controller.selectedPenyaluran.value != null) {
|
||||
controller.exportToPdf(laporan,
|
||||
controller.selectedPenyaluran.value!);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.picture_as_pdf,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'Ekspor PDF',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
Get.toNamed('/laporan-penyaluran/edit',
|
||||
arguments: laporan.id);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.edit, color: Colors.orange, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('Edit',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
_showDeleteConfirmation(context, laporan.id!);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.delete, color: Colors.red, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('Hapus',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog konfirmasi hapus laporan
|
||||
void _showDeleteConfirmation(BuildContext context, String laporanId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Hapus Laporan'),
|
||||
content: const Text('Apakah Anda yakin ingin menghapus laporan ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.deleteLaporan(laporanId);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog pilih penyaluran untuk laporan baru
|
||||
void _showPenyaluranDialog(BuildContext context) {
|
||||
if (controller.penyaluranTanpaLaporan.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Tidak ada penyaluran yang tersedia untuk dibuat laporan. Pastikan ada penyaluran dengan status SELESAI.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Pilih Penyaluran'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.penyaluranTanpaLaporan.length,
|
||||
itemBuilder: (context, index) {
|
||||
final penyaluran = controller.penyaluranTanpaLaporan[index];
|
||||
return ListTile(
|
||||
title: Text(
|
||||
penyaluran.nama ??
|
||||
'Penyaluran #${penyaluran.id?.substring(0, 8)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Tanggal: ${penyaluran.tanggalSelesai != null ? DateFormat('dd/MM/yyyy').format(penyaluran.tanggalSelesai!) : '-'}',
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
// Arahkan ke halaman buat laporan dengan ID penyaluran
|
||||
Get.toNamed('/laporan-penyaluran/create',
|
||||
arguments: penyaluran.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user