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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||
@ -6,7 +7,6 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/laporan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||
|
||||
@ -60,8 +60,8 @@ class PetugasDesaBinding extends Bindings {
|
||||
);
|
||||
|
||||
// Daftarkan controller laporan
|
||||
Get.lazyPut<LaporanController>(
|
||||
() => LaporanController(),
|
||||
Get.lazyPut<LaporanPenyaluranController>(
|
||||
() => LaporanPenyaluranController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ 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/data/models/laporan_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
|
||||
class DetailPenyaluranController extends GetxController {
|
||||
final SupabaseService _supabaseService = Get.find<SupabaseService>();
|
||||
@ -14,10 +16,16 @@ class DetailPenyaluranController extends GetxController {
|
||||
final penyaluran = Rx<PenyaluranBantuanModel?>(null);
|
||||
final skemaBantuan = Rx<SkemaBantuanModel?>(null);
|
||||
final penerimaPenyaluran = <PenerimaPenyaluranModel>[].obs;
|
||||
final laporan = Rx<LaporanPenyaluranModel?>(null);
|
||||
final isLoadingLaporan = false.obs;
|
||||
|
||||
// Status untuk mengetahui apakah petugas desa
|
||||
final isPetugasDesa = false.obs;
|
||||
|
||||
// Tambahkan referensi ke controller laporan
|
||||
LaporanPenyaluranController? laporanController;
|
||||
final RxBool isExporting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -432,6 +440,9 @@ class DetailPenyaluranController extends GetxController {
|
||||
}
|
||||
penerimaPenyaluran.assignAll(penerima);
|
||||
|
||||
// Periksa apakah ada laporan untuk penyaluran ini
|
||||
await checkLaporanPenyaluran(penyaluranId);
|
||||
|
||||
// if (penerima.isNotEmpty) {
|
||||
// print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
|
||||
// }
|
||||
@ -446,6 +457,57 @@ class DetailPenyaluranController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkLaporanPenyaluran(String penyaluranId) async {
|
||||
try {
|
||||
isLoadingLaporan.value = true;
|
||||
|
||||
final response = await _supabaseService.client
|
||||
.from('laporan_penyaluran')
|
||||
.select('*')
|
||||
.eq('penyaluran_bantuan_id', penyaluranId)
|
||||
.maybeSingle();
|
||||
|
||||
if (response != null) {
|
||||
// Laporan ditemukan
|
||||
laporan.value = LaporanPenyaluranModel.fromJson(response);
|
||||
} else {
|
||||
// Tidak ada laporan
|
||||
laporan.value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error saat memeriksa laporan penyaluran: $e');
|
||||
laporan.value = null;
|
||||
} finally {
|
||||
isLoadingLaporan.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToLaporanCreate() {
|
||||
if (penyaluran.value?.id == null) return;
|
||||
|
||||
// Kirim ID penyaluran langsung sebagai argument (String), bukan dalam bentuk Map
|
||||
Get.toNamed('/laporan-penyaluran/create', arguments: penyaluran.value!.id)
|
||||
?.then((value) {
|
||||
if (value == true) {
|
||||
// Refresh data setelah membuat laporan
|
||||
checkLaporanPenyaluran(penyaluran.value!.id!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void navigateToLaporanDetail() {
|
||||
if (laporan.value?.id == null) return;
|
||||
|
||||
// Navigasi ke halaman detail dengan mengirimkan ID sebagai argument
|
||||
Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.value!.id)
|
||||
?.then((value) {
|
||||
if (value == true) {
|
||||
// Refresh data setelah melihat detail laporan
|
||||
checkLaporanPenyaluran(penyaluran.value!.id!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Metode untuk verifikasi penerima berdasarkan QR code
|
||||
Future<bool> verifikasiPenerimaByQrCode(
|
||||
String penyaluranId, String qrHash) async {
|
||||
@ -498,4 +560,53 @@ class DetailPenyaluranController extends GetxController {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk memuat controller laporan
|
||||
Future<void> loadLaporanPenyaluranController() async {
|
||||
if (laporanController == null) {
|
||||
// Cek apakah controller sudah ada di Get
|
||||
if (Get.isRegistered<LaporanPenyaluranController>()) {
|
||||
laporanController = Get.find<LaporanPenyaluranController>();
|
||||
} else {
|
||||
// Jika belum ada, buat instance baru
|
||||
laporanController = Get.put(LaporanPenyaluranController());
|
||||
}
|
||||
}
|
||||
|
||||
// Pastikan data laporan dimuat
|
||||
if (laporan.value != null && penyaluran.value != null) {
|
||||
await laporanController!.fetchLaporanDetail(laporan.value!.id!);
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk export PDF
|
||||
Future<void> exportToPdf() async {
|
||||
if (laporan.value == null || penyaluran.value == null) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Data laporan atau penyaluran tidak tersedia',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isExporting.value = true;
|
||||
try {
|
||||
await loadLaporanPenyaluranController();
|
||||
await laporanController!.exportToPdf(laporan.value!, penyaluran.value!);
|
||||
} catch (e) {
|
||||
print('Error saat export PDF: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengekspor laporan ke PDF',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,197 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
|
||||
class LaporanController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
final SupabaseService _supabaseService = SupabaseService.to;
|
||||
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Indeks kategori yang dipilih untuk filter
|
||||
final RxInt selectedCategoryIndex = 0.obs;
|
||||
|
||||
// Data untuk laporan
|
||||
final RxList<LaporanModel> daftarLaporan = <LaporanModel>[].obs;
|
||||
|
||||
// Filter tanggal
|
||||
final Rx<DateTime?> tanggalMulai = Rx<DateTime?>(null);
|
||||
final Rx<DateTime?> tanggalSelesai = Rx<DateTime?>(null);
|
||||
|
||||
// Controller untuk pencarian
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
UserModel? get user => _authController.user;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Set default tanggal filter (1 bulan terakhir)
|
||||
tanggalSelesai.value = DateTime.now();
|
||||
tanggalMulai.value = DateTime.now().subtract(const Duration(days: 30));
|
||||
loadLaporanData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> loadLaporanData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final laporanData = await _supabaseService.getLaporan(
|
||||
tanggalMulai.value,
|
||||
tanggalSelesai.value,
|
||||
);
|
||||
if (laporanData != null) {
|
||||
daftarLaporan.value =
|
||||
laporanData.map((data) => LaporanModel.fromJson(data)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading laporan data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> generateLaporan(String jenis) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final laporan = LaporanModel(
|
||||
jenis: jenis,
|
||||
tanggalMulai: tanggalMulai.value,
|
||||
tanggalSelesai: tanggalSelesai.value,
|
||||
petugasId: user?.id,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final laporanId =
|
||||
await _supabaseService.generateLaporan(laporan.toJson());
|
||||
|
||||
if (laporanId != null) {
|
||||
await loadLaporanData();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil dibuat',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error generating laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal membuat laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> downloadLaporan(String laporanId) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final url = await _supabaseService.downloadLaporan(laporanId);
|
||||
if (url != null) {
|
||||
// Implementasi download file
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil diunduh',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error downloading laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengunduh laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteLaporan(String laporanId) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await _supabaseService.deleteLaporan(laporanId);
|
||||
await loadLaporanData();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil dihapus',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error deleting laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void setTanggalMulai(DateTime tanggal) {
|
||||
tanggalMulai.value = tanggal;
|
||||
}
|
||||
|
||||
void setTanggalSelesai(DateTime tanggal) {
|
||||
tanggalSelesai.value = tanggal;
|
||||
}
|
||||
|
||||
Future<void> applyFilter() async {
|
||||
await loadLaporanData();
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadLaporanData();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void changeCategory(int index) {
|
||||
selectedCategoryIndex.value = index;
|
||||
}
|
||||
|
||||
List<LaporanModel> getFilteredLaporan() {
|
||||
switch (selectedCategoryIndex.value) {
|
||||
case 0:
|
||||
return daftarLaporan;
|
||||
case 1:
|
||||
return daftarLaporan
|
||||
.where((item) => item.jenis == 'PENYALURAN')
|
||||
.toList();
|
||||
case 2:
|
||||
return daftarLaporan
|
||||
.where((item) => item.jenis == 'STOK_BANTUAN')
|
||||
.toList();
|
||||
case 3:
|
||||
return daftarLaporan.where((item) => item.jenis == 'PENERIMA').toList();
|
||||
default:
|
||||
return daftarLaporan;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart';
|
||||
|
||||
class DetailPenyaluranPage extends StatelessWidget {
|
||||
@ -69,6 +68,9 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
controller.penyaluran.value?.alasanPembatalan != null &&
|
||||
controller.penyaluran.value!.alasanPembatalan!.isNotEmpty)
|
||||
_buildPembatalanSection(context),
|
||||
if (controller.penyaluran.value?.status?.toUpperCase() ==
|
||||
'TERLAKSANA')
|
||||
_buildLaporanSection(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildPenerimaPenyaluranSection(context),
|
||||
const SizedBox(height: 24),
|
||||
@ -1495,6 +1497,220 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanSection(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingLaporan.value) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 8),
|
||||
Text('Memuat data laporan...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppTheme.successColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Laporan Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (controller.laporan.value != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppTheme.successColor.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: AppTheme.successColor,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
if (controller.laporan.value == null)
|
||||
Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_late_outlined,
|
||||
size: 50,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada laporan penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Buat laporan untuk mendokumentasikan hasil penyaluran bantuan',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton.icon(
|
||||
onPressed: controller.navigateToLaporanCreate,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
label: const Text('Buat Laporan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Judul', controller.laporan.value!.judul),
|
||||
_buildInfoRow(
|
||||
'Tanggal Laporan',
|
||||
controller.laporan.value?.tanggalLaporan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
controller.laporan.value!.tanggalLaporan!)
|
||||
: '-',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
controller.navigateToLaporanDetail(),
|
||||
icon: const Icon(Icons.visibility),
|
||||
label: const Text('Lihat Detail'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.primaryColor,
|
||||
side: const BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (controller.laporan.value?.beritaAcaraUrl != null &&
|
||||
controller
|
||||
.laporan.value!.beritaAcaraUrl!.isNotEmpty)
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton.icon(
|
||||
onPressed: controller.isExporting.value
|
||||
? null
|
||||
: () => controller.exportToPdf(),
|
||||
icon: controller.isExporting.value
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
label: Text(controller.isExporting.value
|
||||
? 'Mengekspor...'
|
||||
: 'Unduh PDF'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12),
|
||||
disabledBackgroundColor:
|
||||
AppTheme.successColor.withOpacity(0.7),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<PenerimaPenyaluranModel> _getFilteredPenerima() {
|
||||
final query = searchQuery.value;
|
||||
final status = statusFilter.value;
|
||||
|
@ -179,18 +179,25 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
backgroundImage: controller.user?.avatar != null &&
|
||||
controller.user!.avatar!.isNotEmpty
|
||||
? NetworkImage(controller.user!.avatar!)
|
||||
: null,
|
||||
child: controller.user?.avatar == null ||
|
||||
controller.user!.avatar!.isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
controller.nama,
|
||||
controller.user?.name ?? 'Petugas Desa',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@ -198,203 +205,106 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Petugas Desa',
|
||||
controller.user?.desa?.nama != null
|
||||
? '${controller.user?.role} - ${controller.user!.desa!.nama}'
|
||||
: controller.user?.role ?? 'PETUGAS_DESA',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: Colors.white.withAlpha(200),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
selected: controller.activeTabIndex.value == 0,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(0);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.calendar_today_outlined),
|
||||
title: const Text('Penyaluran'),
|
||||
selected: controller.activeTabIndex.value == 1,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() => ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.handshake_outlined),
|
||||
if (controller.jumlahMenunggu.value > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahMenunggu.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Penitipan'),
|
||||
selected: controller.activeTabIndex.value == 2,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() {
|
||||
final int jumlahPengaduanDiproses = controller.jumlahDiproses.value;
|
||||
|
||||
return ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.report_problem_outlined),
|
||||
// Selalu tampilkan badge untuk debugging
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
jumlahPengaduanDiproses.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Pengaduan'),
|
||||
selected: controller.activeTabIndex.value == 3,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(3);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Stok Bantuan'),
|
||||
selected: controller.activeTabIndex.value == 4,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(4);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people_outline),
|
||||
title: const Text('Daftar Penerima'),
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
selected: controller.activeTabIndex.value == 0,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(0);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.handshake_outlined),
|
||||
title: const Text('Penyaluran'),
|
||||
selected: controller.activeTabIndex.value == 1,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(1);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Penitipan'),
|
||||
selected: controller.activeTabIndex.value == 2,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(2);
|
||||
},
|
||||
),
|
||||
Obx(() => ListTile(
|
||||
leading: controller.jumlahDiproses.value > 0
|
||||
? Badge(
|
||||
label: Text(controller.jumlahDiproses.value.toString()),
|
||||
backgroundColor: Colors.red,
|
||||
child: const Icon(Icons.support_outlined),
|
||||
)
|
||||
: const Icon(Icons.support_outlined),
|
||||
title: const Text('Pengaduan'),
|
||||
selected: controller.activeTabIndex.value == 3,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(3);
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_outlined),
|
||||
title: const Text('Stok Bantuan'),
|
||||
selected: controller.activeTabIndex.value == 4,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(4);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: const Text('Kelola Penerima'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-penerima');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.volunteer_activism_outlined),
|
||||
title: const Text('Daftar Donatur'),
|
||||
leading: const Icon(Icons.people_outlined),
|
||||
title: const Text('Kelola Donatur'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-donatur');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.notifications_outlined),
|
||||
if (controller.jumlahNotifikasiBelumDibaca.value > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahNotifikasiBelumDibaca.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Notifikasi'),
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: const Text('Laporan Penyaluran'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NotifikasiView(),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/laporan-penyaluran');
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
onTap: () {
|
||||
// Navigasi ke halaman profil
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Pengaturan'),
|
||||
onTap: () {
|
||||
// Navigasi ke halaman pengaturan
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
|
@ -98,7 +98,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'terlaksana' : 'batal terlaksana'}',
|
||||
'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'Terlaksana' : 'Batal'}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -108,6 +108,11 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
badgeColor: Colors.orange,
|
||||
onTap: () => controller.changeTab(2),
|
||||
),
|
||||
DrawerMenuItem(
|
||||
icon: Icons.description_outlined,
|
||||
title: 'Laporan Penyaluran',
|
||||
onTap: () => Get.toNamed('/laporan-penyaluran'),
|
||||
),
|
||||
],
|
||||
)),
|
||||
body: Obx(() {
|
||||
|
Reference in New Issue
Block a user