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