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:
@ -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,
|
||||
|
Reference in New Issue
Block a user