From cf43c472bd7150d11b123d53adb40390a430316d Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Sun, 16 Mar 2025 09:43:28 +0700 Subject: [PATCH] Tambahkan dependensi flutter_staggered_animations versi 1.1.1 ke dalam pubspec.yaml dan pubspec.lock. Perbarui DetailPenyaluranPage untuk menggunakan animasi baru, termasuk penambahan fitur scroll ke atas dan loading state yang lebih informatif. Modifikasi tampilan dan logika untuk meningkatkan pengalaman pengguna saat menampilkan daftar penerima penyaluran. --- .../arm64-v8a/configure_fingerprint.bin | 24 +- .../armeabi-v7a/configure_fingerprint.bin | 24 +- .../626b5o2n/x86/configure_fingerprint.bin | 24 +- .../626b5o2n/x86_64/configure_fingerprint.bin | 24 +- .../penyaluran/detail_penyaluran_page.dart | 1165 +++++++++++++---- .../components/calendar_view_widget.dart | 451 +++++-- pubspec.lock | 8 + pubspec.yaml | 1 + 8 files changed, 1358 insertions(+), 363 deletions(-) diff --git a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin index 25c9528..7bc2885 100644 --- a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  2 2 +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ۈ2 2  -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  2  2~ +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  ۈ2  2~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  2 2 +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  ۈ2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  2 2p +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  ۈ2 2p n -lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  2 2t +lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  ۈ2 2t r -pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  2y +pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  ۈ2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  2 K 2z +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  ۈ2 K 2z x -vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  2 ~ +vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  ۈ2 ~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  2 +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  ۈ2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  2  2w +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  ۈ2  2w u -sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  2  ( 2| +sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  ۈ2  ( 2| z -xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\symbol_folder_index.txt  2  o 2 \ No newline at end of file +xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\symbol_folder_index.txt  ۈ2  o 2 \ No newline at end of file diff --git a/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin index b7304f7..5411842 100644 --- a/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  Ÿ2 2 +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ܈2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt  Ÿ2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt  ܈2  2 ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json  Ÿ2 2 +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json  ܈2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json  Ÿ2 2r +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json  ܈2 2r p -nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja  Ÿ2 2v +nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja  ܈2 2v t -rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  Ÿ2{ +rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  ܈2{ y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  Ÿ2 K 2| +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  ܈2 K 2| z -xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json  Ÿ2  +xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json  ܈2  ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin  Ÿ2 +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin  ܈2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt  Ÿ2  2y +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt  ܈2  2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json  Ÿ2  ( 2~ +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json  ܈2  ( 2~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\symbol_folder_index.txt  Ÿ2  q 2 \ No newline at end of file +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\symbol_folder_index.txt  ܈2  q 2 \ No newline at end of file diff --git a/android/app/.cxx/Debug/626b5o2n/x86/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/x86/configure_fingerprint.bin index 0eda78d..fe4a425 100644 --- a/android/app/.cxx/Debug/626b5o2n/x86/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/x86/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  Ǹ2 2{ +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ݈2 2{ y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  Ǹ2  2x +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  ݈2  2x v -tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  Ǹ2 2} +tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  ݈2 2} { -yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  Ǹ2 2j +yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  ݈2 2j h -fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  Ǹ2 2n +fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  ݈2 2n l -jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  Ǹ2s +jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  ݈2s q -oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  Ǹ2 K 2t +oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  ݈2 K 2t r -pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json  Ǹ2 x +pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json  ݈2 x v -tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  Ǹ2 +tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  ݈2 ~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  Ǹ2  2q +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  ݈2  2q o -mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json  Ǹ2  ( 2v +mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json  ݈2  ( 2v t -rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\symbol_folder_index.txt  Ǹ2  i 2 \ No newline at end of file +rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\symbol_folder_index.txt  ݈2  i 2 \ No newline at end of file diff --git a/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin index 1f1fe57..abd7eab 100644 --- a/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ˸2 2~ +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ވ2 2~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  ˸2  2{ +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  ވ2  2{ y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  ˸2 2 +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  ވ2 2 ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  ˸2 2m +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  ވ2 2m k -iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  ˸2 2q +iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  ވ2 2q o -mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  ˸2v +mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  ވ2v t -rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  ˸2 K 2w +rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  ވ2 K 2w u -sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  ˸2 { +sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  ވ2 { y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  ˸2 +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  ވ2   -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  ˸2  2t +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  ވ2  2t r -pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  ˸2  ( 2y +pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  ވ2  ( 2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\symbol_folder_index.txt  ˸2  l 2 \ No newline at end of file +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\symbol_folder_index.txt  ވ2  l 2 \ No newline at end of file diff --git a/lib/app/modules/penyaluran/detail_penyaluran_page.dart b/lib/app/modules/penyaluran/detail_penyaluran_page.dart index 4b18999..a4aa846 100644 --- a/lib/app/modules/penyaluran/detail_penyaluran_page.dart +++ b/lib/app/modules/penyaluran/detail_penyaluran_page.dart @@ -7,17 +7,32 @@ import 'package:image_picker/image_picker.dart'; import 'package:penyaluran_app/app/modules/penyaluran/konfirmasi_penerima_page.dart'; import 'package:penyaluran_app/app/utils/date_formatter.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class DetailPenyaluranPage extends StatelessWidget { final controller = Get.put(DetailPenyaluranController()); final ImagePicker _picker = ImagePicker(); final searchController = TextEditingController(); final RxString searchQuery = ''.obs; + final RxString statusFilter = 'SEMUA'.obs; + final RxBool showAllItems = false.obs; + final int initialItemCount = 4; + final ScrollController scrollController = ScrollController(); + final RxBool showScrollToTop = false.obs; DetailPenyaluranPage({super.key}); @override Widget build(BuildContext context) { + // Tambahkan listener untuk scroll controller + scrollController.addListener(() { + if (scrollController.offset > 300) { + showScrollToTop.value = true; + } else { + showScrollToTop.value = false; + } + }); + return Scaffold( appBar: AppBar( title: const Text('Detail Penyaluran'), @@ -29,7 +44,7 @@ class DetailPenyaluranPage extends StatelessWidget { ), body: Obx(() { if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); + return _buildLoadingState(); } if (controller.penyaluran.value == null) { @@ -39,8 +54,11 @@ class DetailPenyaluranPage extends StatelessWidget { } return RefreshIndicator( - onRefresh: controller.refreshData, + onRefresh: () async { + await controller.refreshData(); + }, child: SingleChildScrollView( + controller: scrollController, padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -62,6 +80,20 @@ class DetailPenyaluranPage extends StatelessWidget { ), ); }), + floatingActionButton: Obx(() => showScrollToTop.value + ? FloatingActionButton( + mini: true, + backgroundColor: AppTheme.primaryColor, + child: const Icon(Icons.arrow_upward), + onPressed: () { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + ) + : const SizedBox.shrink()), bottomNavigationBar: Obx(() { final status = controller.penyaluran.value?.status?.toUpperCase() ?? ''; if (status == 'AKTIF' || @@ -74,6 +106,25 @@ class DetailPenyaluranPage extends StatelessWidget { ); } + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + 'Memuat data penyaluran...', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + ), + ), + ], + ), + ); + } + Widget _buildInfoCard(BuildContext context) { final penyaluran = controller.penyaluran.value!; final skema = controller.skemaBantuan.value; @@ -224,190 +275,699 @@ class DetailPenyaluranPage extends StatelessWidget { Widget _buildPenerimaPenyaluranSection(BuildContext context) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan judul dan jumlah penerima + Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: 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, + const Row( + children: [ + Icon( + Icons.people_alt_rounded, + color: Colors.white, + size: 24, + ), + SizedBox(width: 8), + Text( + 'Daftar Penerima', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold, - color: AppTheme.secondaryColor, + color: Colors.white, + ), + ), + ], + ), + Obx(() => Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_getFilteredPenerima().length} Orang', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), ), )), ], ), - 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, + // Statistik penerima + Obx(() => _buildStatistikPenerima(context)), + + // Search field dengan filter status + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Column( + children: [ + // Search field dengan icon dan tombol hapus + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Cari nama, NIK, atau alamat...', + prefixIcon: const Icon(Icons.search, + color: AppTheme.primaryColor), + suffixIcon: Obx(() => searchQuery.value.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + searchController.clear(); + searchQuery.value = ''; + }, + ) + : const SizedBox.shrink()), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppTheme.primaryColor, width: 1), + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + ), + onChanged: (value) { + searchQuery.value = value.toLowerCase(); + }, + ), ), - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: const EdgeInsets.symmetric(vertical: 0), - ), - onChanged: (value) { - searchQuery.value = value.toLowerCase(); - }, + + const SizedBox(height: 16), + + // Filter status dengan label + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterChip('Semua', true), + const SizedBox(width: 8), + _buildFilterChip('Sudah Menerima', false), + const SizedBox(width: 8), + _buildFilterChip('Belum Menerima', false), + ], + ), + ), + ], ), + ), - const SizedBox(height: 16), + // Daftar penerima + Obx(() { + final filteredList = _getFilteredPenerima(); - // Daftar penerima - Obx(() { - final filteredList = _getFilteredPenerima(); + if (filteredList.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.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, + ), + ), + const SizedBox(height: 8), + Text( + 'Coba ubah filter pencarian', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + ); + } - 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, - ), - ), - ], + // Tentukan jumlah item yang akan ditampilkan + final itemsToShow = + showAllItems.value || filteredList.length <= initialItemCount + ? filteredList + : filteredList.sublist(0, initialItemCount); + + // Tentukan jumlah kolom berdasarkan lebar layar + final screenWidth = MediaQuery.of(context).size.width; + final crossAxisCount = screenWidth > 600 ? 2 : 1; + final childAspectRatio = screenWidth > 600 ? 2.5 : 2.2; + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: AnimationLimiter( + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + childAspectRatio: childAspectRatio, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: itemsToShow.length, + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredGrid( + position: index, + duration: const Duration(milliseconds: 375), + columnCount: crossAxisCount, + child: ScaleAnimation( + child: FadeInAnimation( + child: _buildPenerimaItem( + context, itemsToShow[index]), + ), + ), + ); + }, ), ), - ); - } + ), - 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]); - }, - ); - }), - ], - ), + // Tombol untuk menampilkan lebih banyak atau lebih sedikit item + if (filteredList.length > initialItemCount) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextButton.icon( + onPressed: () { + showAllItems.value = !showAllItems.value; + }, + icon: Icon( + showAllItems.value + ? Icons.expand_less + : Icons.expand_more, + color: AppTheme.primaryColor, + ), + label: Text( + showAllItems.value + ? 'Tampilkan Lebih Sedikit' + : 'Lihat Semua (${filteredList.length})', + style: const TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + }), + ], ), ); } - List _getFilteredPenerima() { - final query = searchQuery.value; - if (query.isEmpty) { - return controller.penerimaPenyaluran; + Widget _buildStatistikPenerima(BuildContext context) { + // Hitung jumlah yang sudah dan belum menerima + int totalPenerima = controller.penerimaPenyaluran.length; + int sudahMenerima = controller.penerimaPenyaluran + .where((item) => item.statusPenerimaan?.toUpperCase() == 'DITERIMA') + .length; + int belumMenerima = totalPenerima - sudahMenerima; + + // Hitung persentase + double persentaseSudah = + totalPenerima > 0 ? (sudahMenerima / totalPenerima) * 100 : 0; + + // Tentukan warna berdasarkan persentase + Color progressColor = persentaseSudah > 75 + ? AppTheme.successColor + : persentaseSudah > 50 + ? AppTheme.warningColor + : AppTheme.errorColor; + + // Tentukan lebar layar untuk responsivitas + final screenWidth = MediaQuery.of(context).size.width; + final isWideScreen = screenWidth > 600; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + bottom: BorderSide(color: Colors.grey.shade200, width: 1), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.insert_chart_outlined, + size: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 8), + Text( + 'Progres Penyaluran', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + ], + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: progressColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: progressColor.withOpacity(0.3)), + ), + child: Text( + '${persentaseSudah.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: progressColor, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Progress bar dengan label + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Sudah Menerima', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), + ), + Text( + '$sudahMenerima dari $totalPenerima', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 6), + Stack( + children: [ + // Background progress bar + Container( + height: 12, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + ), + // Foreground progress bar + FractionallySizedBox( + widthFactor: persentaseSudah / 100, + child: Container( + height: 12, + decoration: BoxDecoration( + color: progressColor, + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: progressColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Statistik detail + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: _buildStatistikItem( + 'Sudah Menerima', + sudahMenerima, + AppTheme.successColor, + Icons.check_circle, + )), + const SizedBox(width: 6), + Expanded( + child: _buildStatistikItem( + 'Belum Menerima', + belumMenerima, + AppTheme.warningColor, + Icons.pending, + )), + ], + ) + ], + ), + ); + } + + Widget _buildStatistikItem( + String label, int value, Color color, IconData icon) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 14), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$value', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFilterChip(String label, bool isSelected) { + String filterValue; + int count = 0; + + // Konversi label ke nilai filter dan hitung jumlah + if (label == 'Semua') { + filterValue = 'SEMUA'; + count = controller.penerimaPenyaluran.length; + } else if (label == 'Sudah Menerima') { + filterValue = 'DITERIMA'; + count = controller.penerimaPenyaluran + .where((item) => item.statusPenerimaan?.toUpperCase() == 'DITERIMA') + .length; + } else { + filterValue = 'BELUM'; + count = controller.penerimaPenyaluran + .where((item) => item.statusPenerimaan?.toUpperCase() != 'DITERIMA') + .length; } - return controller.penerimaPenyaluran.where((item) { - final warga = item.warga; - if (warga == null) return false; + // Cek apakah filter ini yang aktif + isSelected = statusFilter.value == filterValue; - final nama = warga['nama_lengkap']?.toString().toLowerCase() ?? ''; - final nik = warga['nik']?.toString().toLowerCase() ?? ''; - final alamat = warga['alamat']?.toString().toLowerCase() ?? ''; - final status = item.statusPenerimaan?.toLowerCase() ?? ''; + // Tentukan icon berdasarkan jenis filter + IconData filterIcon; + if (label == 'Semua') { + filterIcon = Icons.list_alt; + } else if (label == 'Sudah Menerima') { + filterIcon = Icons.check_circle; + } else { + filterIcon = Icons.pending; + } - return nama.contains(query) || - nik.contains(query) || - alamat.contains(query) || - status.contains(query); - }).toList(); + return FilterChip( + avatar: Icon( + filterIcon, + size: 16, + color: isSelected ? Colors.white : AppTheme.primaryColor, + ), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(label), + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.3) + : AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.white : AppTheme.primaryColor, + ), + ), + ), + ], + ), + selected: isSelected, + // checkmarkColor: Colors.white, + showCheckmark: false, + selectedColor: AppTheme.primaryColor, + labelStyle: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + backgroundColor: Colors.white, + elevation: isSelected ? 0 : 1, + shadowColor: Colors.grey.withOpacity(0.2), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: isSelected ? Colors.transparent : Colors.grey.shade300, + width: 1, + ), + ), + onSelected: (selected) { + if (selected) { + statusFilter.value = filterValue; + } + }, + ); } Widget _buildPenerimaItem( BuildContext context, PenerimaPenyaluranModel item) { final warga = item.warga; + final bool sudahMenerima = + item.statusPenerimaan?.toUpperCase() == 'DITERIMA'; + final Color cardColor = Colors.white; + final Color borderColor = Colors.grey.shade300; - 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, + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: borderColor, width: 1.5), + ), + color: cardColor, + child: InkWell( + onTap: () => _showDetailPenerima(context, item), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Avatar + CircleAvatar( + radius: 24, + backgroundColor: sudahMenerima + ? AppTheme.successColor.withOpacity(0.2) + : AppTheme.primaryColor.withOpacity(0.1), + child: Text( + warga != null && warga['nama_lengkap'] != null + ? warga['nama_lengkap'] + .toString() + .substring(0, 1) + .toUpperCase() + : '?', + style: TextStyle( + fontWeight: FontWeight.bold, + color: sudahMenerima + ? AppTheme.successColor + : AppTheme.primaryColor, + fontSize: 20, + ), ), ), - ), - const SizedBox(width: 12), + const SizedBox(width: 12), - // Info penerima - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + // Informasi penerima + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Nama dan NIK + Text( + warga != null + ? warga['nama_lengkap'] ?? 'Nama tidak tersedia' + : 'Nama tidak tersedia', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + 'NIK: ${warga != null ? warga['nik'] ?? '-' : '-'}', + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 4), + _buildStatusChipNew(item.statusPenerimaan ?? '-'), + ], + ), + ), + + // Status dan icon + Column( + mainAxisSize: MainAxisSize.min, 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], - ), - ), + Icon(Icons.arrow_forward_ios, + size: 14, + color: + sudahMenerima ? AppTheme.successColor : Colors.grey), ], ), - ), - - // Status chip - _buildStatusChip(item.statusPenerimaan ?? '-'), - - const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey), - ], + ], + ), ), ), ); } + Widget _buildStatusChipNew(String status) { + Color backgroundColor; + Color textColor = Colors.white; + String statusText = _getStatusPenerimaanText(status); + IconData iconData; + + // Konversi status ke format yang diinginkan + if (status.toUpperCase() == 'DITERIMA') { + backgroundColor = AppTheme.successColor; + statusText = 'Sudah Menerima'; + iconData = Icons.check_circle; + } else { + // Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA + backgroundColor = AppTheme.warningColor; + statusText = 'Belum Menerima'; + iconData = Icons.pending; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + iconData, + color: textColor, + size: 12, + ), + const SizedBox(width: 4), + Text( + statusText, + style: TextStyle( + color: textColor, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + Widget _buildStatusBadge(String status) { Color backgroundColor; Color textColor = Colors.white; @@ -447,38 +1007,6 @@ class DetailPenyaluranPage extends StatelessWidget { ); } - Widget _buildStatusChip(String status) { - Color backgroundColor; - Color textColor = Colors.white; - String statusText = _getStatusPenerimaanText(status); - - // Konversi status ke format yang diinginkan - if (status.toUpperCase() == 'DITERIMA') { - 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() ?? ''; @@ -660,6 +1188,10 @@ class DetailPenyaluranPage extends StatelessWidget { void _showDetailPenerima( BuildContext context, PenerimaPenyaluranModel penerima) { final warga = penerima.warga; + final bool sudahMenerima = + penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA'; + final Color statusColor = + sudahMenerima ? AppTheme.successColor : AppTheme.warningColor; showModalBottomSheet( context: context, @@ -678,6 +1210,7 @@ class DetailPenyaluranPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + // Handle untuk drag Center( child: Container( width: 50, @@ -689,72 +1222,195 @@ class DetailPenyaluranPage extends StatelessWidget { ), ), const SizedBox(height: 20), - const Text( - 'Biodata Singkat', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + + // Header dengan avatar dan nama + Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: statusColor.withOpacity(0.2), + child: Text( + warga != null && warga['nama_lengkap'] != null + ? warga['nama_lengkap'] + .toString() + .substring(0, 1) + .toUpperCase() + : '?', + style: TextStyle( + fontWeight: FontWeight.bold, + color: statusColor, + fontSize: 24, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + warga != null + ? warga['nama_lengkap'] ?? 'Nama tidak tersedia' + : 'Nama tidak tersedia', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 4), + _buildStatusChipNew(penerima.statusPenerimaan ?? '-'), + ], + ), + ), + ], + ), + + const SizedBox(height: 24), + + // Informasi biodata + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Biodata Singkat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const Divider(height: 24), + if (warga != null) ...[ + _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), - 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', - DateFormatter.formatDate(penerima.tanggalPenerimaan!)), - if (penerima.jumlahBantuan != null) - _buildInfoRow( - 'Jumlah Bantuan', penerima.jumlahBantuan.toString()), - if (penerima.keterangan != null && - penerima.keterangan!.isNotEmpty) - _buildInfoRow('Keterangan', penerima.keterangan!), + + const SizedBox(height: 16), + + // Informasi penerimaan + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + sudahMenerima ? Icons.check_circle : Icons.pending, + color: statusColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Informasi Penerimaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ], + ), + const Divider(height: 24), + _buildInfoRow( + 'Status Penerimaan', + _getStatusPenerimaanText( + penerima.statusPenerimaan ?? '-')), + if (penerima.tanggalPenerimaan != null) + _buildInfoRow( + 'Tanggal Penerimaan', + DateFormatter.formatDate( + penerima.tanggalPenerimaan!)), + if (penerima.jumlahBantuan != null) + _buildInfoRow('Jumlah Bantuan', + penerima.jumlahBantuan.toString()), + if (penerima.keterangan != null && + penerima.keterangan!.isNotEmpty) + _buildInfoRow('Keterangan', penerima.keterangan!), + ], + ), + ), + + // Bukti penerimaan 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, + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - ), - 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'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Bukti Penerimaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, ), - ); - }, + ), + const SizedBox(height: 12), + 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), + + // Tombol konfirmasi penerimaan if (controller.penyaluran.value?.status?.toUpperCase() == 'AKTIF' && penerima.statusPenerimaan?.toUpperCase() != 'DITERIMA') ...[ - const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( @@ -768,6 +1424,10 @@ class DetailPenyaluranPage extends StatelessWidget { ), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.successColor, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), onPressed: () { Navigator.pop(context); @@ -775,12 +1435,22 @@ class DetailPenyaluranPage extends StatelessWidget { }, ), ), + const SizedBox(height: 10), ], - const SizedBox(height: 10), + + // Tombol tutup SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade200, + foregroundColor: Colors.black87, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), child: const Text( 'Tutup', style: TextStyle( @@ -893,4 +1563,43 @@ class DetailPenyaluranPage extends StatelessWidget { ), ); } + + List _getFilteredPenerima() { + final query = searchQuery.value; + final status = statusFilter.value; + + // Filter dasar berdasarkan query pencarian + List filteredList = controller.penerimaPenyaluran; + + if (query.isNotEmpty) { + filteredList = filteredList.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 statusPenerimaan = item.statusPenerimaan?.toLowerCase() ?? ''; + + return nama.contains(query) || + nik.contains(query) || + alamat.contains(query) || + statusPenerimaan.contains(query); + }).toList(); + } + + // Filter tambahan berdasarkan status + if (status != 'SEMUA') { + filteredList = filteredList.where((item) { + if (status == 'DITERIMA') { + return item.statusPenerimaan?.toUpperCase() == 'DITERIMA'; + } else { + // Filter untuk yang belum menerima + return item.statusPenerimaan?.toUpperCase() != 'DITERIMA'; + } + }).toList(); + } + + return filteredList; + } } diff --git a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart index 98c25cc..0838906 100644 --- a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart +++ b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart @@ -43,7 +43,7 @@ class CalendarViewWidget extends StatelessWidget { ), ), SizedBox( - height: 500, + height: MediaQuery.of(context).size.height * 0.65, child: Obx(() { return SfCalendar( view: CalendarView.month, @@ -52,8 +52,8 @@ class CalendarViewWidget extends StatelessWidget { monthViewSettings: MonthViewSettings( appointmentDisplayMode: MonthAppointmentDisplayMode.indicator, showAgenda: true, - agendaViewHeight: 250, - agendaItemHeight: 70, + agendaViewHeight: MediaQuery.of(context).size.height * 0.3, + agendaItemHeight: 60, dayFormat: 'EEE', numberOfWeeksInView: 6, appointmentDisplayCount: 3, @@ -119,6 +119,7 @@ class CalendarViewWidget extends StatelessWidget { // } } }, + appointmentBuilder: _appointmentBuilder, ); }), ), @@ -260,16 +261,23 @@ class CalendarViewWidget extends StatelessWidget { jadwalDate.isBefore(lastDayOfMonth.add(const Duration(days: 1)))) { Color appointmentColor; - // Periksa status jadwal - if (jadwal.status == 'SELESAI') { - appointmentColor = Colors.grey; - } else if (jadwal.status == 'BERLANGSUNG') { - appointmentColor = Colors.green; - } else if (jadwal.status == 'DIJADWALKAN' || - jadwal.status == 'DISETUJUI') { - appointmentColor = AppTheme.primaryColor; - } else { - appointmentColor = Colors.orange; + // Periksa status jadwal menggunakan switch-case untuk konsistensi + switch (jadwal.status?.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + appointmentColor = AppTheme.processedColor; + break; + case 'AKTIF': + appointmentColor = AppTheme.scheduledColor; + break; + case 'TERLAKSANA': + appointmentColor = AppTheme.completedColor; + break; + case 'BATALTERLAKSANA': + appointmentColor = AppTheme.errorColor; + break; + default: + appointmentColor = AppTheme.infoColor; } appointments.add( @@ -296,99 +304,368 @@ class CalendarViewWidget extends StatelessWidget { final String formattedDate = DateTimeHelper.formatDateIndonesian(appointment.startTime); + // Dapatkan status dari ID jadwal + String? status = _getStatusFromAppointmentId(appointment.id); + showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appointment.subject, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Row( + builder: (context) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + padding: const EdgeInsets.all(20), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.calendar_today, size: 16), - const SizedBox(width: 8), - Text( - formattedDate, - style: const TextStyle(fontSize: 16), + Row( + children: [ + Expanded( + child: Text( + appointment.subject, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + if (status != null) _buildStatusBadge(status), + ], ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Icon(Icons.access_time, size: 16), - const SizedBox(width: 8), - Text( - '${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB', - style: const TextStyle(fontSize: 16), - ), - ], - ), - if (appointment.location != null && - appointment.location!.isNotEmpty) ...[ - const SizedBox(height: 8), - Row( - children: [ - const Icon(Icons.location_on, size: 16), - const SizedBox(width: 8), - Expanded( - child: Text( - appointment.location!, + const SizedBox(height: 10), + if (status != null) ...[ + Row( + children: [ + Icon( + _getStatusIcon(status), + size: 16, + color: _getStatusColor(status), + ), + const SizedBox(width: 8), + Text( + 'Status: ${_getStatusText(status)}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: _getStatusColor(status), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + Row( + children: [ + const Icon(Icons.calendar_today, size: 16), + const SizedBox(width: 8), + Text( + formattedDate, style: const TextStyle(fontSize: 16), ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.access_time, size: 16), + const SizedBox(width: 8), + Text( + '${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB', + style: const TextStyle(fontSize: 16), + ), + ], + ), + if (appointment.location != null && + appointment.location!.isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.location_on, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + appointment.location!, + style: const TextStyle(fontSize: 16), + ), + ), + ], ), ], - ), - ], - if (appointment.notes != null && appointment.notes!.isNotEmpty) ...[ - const SizedBox(height: 16), - const Text( - 'Deskripsi:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - appointment.notes!, - style: const TextStyle(fontSize: 16), - ), - ], - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + if (appointment.notes != null && + appointment.notes!.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + 'Deskripsi:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + appointment.notes!, + style: const TextStyle(fontSize: 16), + ), + ], + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Tutup'), ), ), - child: const Text('Tutup'), - ), + ], ), - ], + ), ), ), ); } + + // Builder untuk kustomisasi tampilan appointment pada agenda view + Widget _appointmentBuilder( + BuildContext context, CalendarAppointmentDetails details) { + final Appointment appointment = details.appointments.first; + String? status = _getStatusFromAppointmentId(appointment.id); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: appointment.color, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text( + appointment.subject, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (status != null) + Icon( + _getStatusIcon(status), + color: Colors.white, + size: 13, + ), + const SizedBox(width: 4), + if (status != null) ...[ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + _getStatusText(status), + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 3), + if (appointment.location != null && appointment.location!.isNotEmpty) + Row( + children: [ + const Icon( + Icons.location_on, + color: Colors.white70, + size: 10, + ), + const SizedBox(width: 3), + Expanded( + child: Text( + appointment.location!, + style: const TextStyle( + fontSize: 10, + color: Colors.white70, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 3), + Row( + children: [ + const Icon( + Icons.access_time, + color: Colors.white70, + size: 10, + ), + const SizedBox(width: 3), + Text( + '${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} WIB', + style: const TextStyle( + fontSize: 10, + color: Colors.white70, + ), + ), + ], + ), + ], + ), + ); + } + + // Mendapatkan status jadwal berdasarkan ID + String? _getStatusFromAppointmentId(Object? id) { + if (id == null) return null; + + String jadwalId = id.toString(); + + // Cari jadwal dengan ID yang sesuai + for (var jadwal in [ + ...controller.jadwalHariIni, + ...controller.jadwalMendatang, + ...controller.jadwalTerlaksana + ]) { + if (jadwal.id == jadwalId) { + return jadwal.status; + } + } + + return null; + } + + // Widget untuk menampilkan badge status + Widget _buildStatusBadge(String status) { + Color backgroundColor; + Color textColor = Colors.white; + String statusText = _getStatusText(status); + + switch (status.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + backgroundColor = AppTheme.processedColor; + break; + case 'AKTIF': + backgroundColor = AppTheme.scheduledColor; + break; + case 'TERLAKSANA': + backgroundColor = AppTheme.completedColor; + break; + case 'BATALTERLAKSANA': + backgroundColor = AppTheme.errorColor; + break; + default: + backgroundColor = AppTheme.infoColor; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getStatusIcon(status), + color: textColor, + size: 12, + ), + const SizedBox(width: 4), + Text( + statusText, + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + // Mendapatkan warna status yang sesuai + Color _getStatusColor(String status) { + switch (status.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + return AppTheme.processedColor; + case 'AKTIF': + return AppTheme.scheduledColor; + case 'TERLAKSANA': + return AppTheme.completedColor; + case 'BATALTERLAKSANA': + return AppTheme.errorColor; + default: + return AppTheme.infoColor; + } + } + + // Mendapatkan ikon status yang sesuai + IconData _getStatusIcon(String status) { + switch (status.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + return Icons.event_available; + case 'AKTIF': + return Icons.play_circle_fill; + case 'TERLAKSANA': + return Icons.check_circle; + case 'BATALTERLAKSANA': + return Icons.cancel; + default: + return Icons.info_outline; + } + } + + // Mendapatkan teks status yang sesuai + String _getStatusText(String status) { + switch (status.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + return 'Dijadwalkan'; + case 'AKTIF': + return 'Aktif'; + case 'TERLAKSANA': + return 'Terlaksana'; + case 'BATALTERLAKSANA': + return 'Batal'; + default: + return status; + } + } } class _AppointmentDataSource extends CalendarDataSource { diff --git a/pubspec.lock b/pubspec.lock index b69e438..3c8a7c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -267,6 +267,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.1" + flutter_staggered_animations: + dependency: "direct main" + description: + name: flutter_staggered_animations + sha256: "81d3c816c9bb0dca9e8a5d5454610e21ffb068aedb2bde49d2f8d04f75538351" + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5e8dadc..bd83019 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: flutter_localizations: sdk: flutter + flutter_staggered_animations: ^1.1.1 dev_dependencies: flutter_test: