From 88bef1c8e1441767cae6eede17a887149a32da29 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Wed, 26 Mar 2025 08:33:55 +0700 Subject: [PATCH] Perbarui model PenitipanBantuan untuk menggunakan stok_bantuan sebagai kategori bantuan. Tambahkan rute baru untuk Donatur dengan beberapa halaman terkait, termasuk donaturDashboard, donaturSkema, donaturJadwal, donaturPenitipan, dan donaturLaporan. Modifikasi rute dan tampilan untuk meningkatkan navigasi dan pengalaman pengguna. --- DonaturDashboardController()) | 0 .../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 +- .../data/models/penitipan_bantuan_model.dart | 4 +- .../donatur/bindings/donatur_binding.dart | 20 + .../donatur_dashboard_controller.dart | 457 +++++++ .../donatur/views/donatur_dashboard_view.dart | 774 +++++++++++ .../donatur/views/donatur_jadwal_view.dart | 322 +++++ .../donatur/views/donatur_laporan_view.dart | 409 ++++++ .../donatur/views/donatur_penitipan_view.dart | 1150 +++++++++++++++++ .../views/donatur_riwayat_penitipan_view.dart | 393 ++++++ .../donatur/views/donatur_skema_view.dart | 274 ++++ .../modules/donatur/views/donatur_view.dart | 326 +++++ lib/app/routes/app_pages.dart | 53 +- lib/app/routes/app_routes.dart | 8 + 17 files changed, 4233 insertions(+), 53 deletions(-) create mode 100644 DonaturDashboardController()) create mode 100644 lib/app/modules/donatur/bindings/donatur_binding.dart create mode 100644 lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart create mode 100644 lib/app/modules/donatur/views/donatur_dashboard_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_jadwal_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_laporan_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_penitipan_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_riwayat_penitipan_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_skema_view.dart create mode 100644 lib/app/modules/donatur/views/donatur_view.dart diff --git a/DonaturDashboardController()) b/DonaturDashboardController()) new file mode 100644 index 0000000..e69de29 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 a8512e8..1196bbf 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 62bb516..6506592 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 35bb6c6..5c0cdfe 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 379d475..0c01600 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/data/models/penitipan_bantuan_model.dart b/lib/app/data/models/penitipan_bantuan_model.dart index 805c95a..e9eafe9 100644 --- a/lib/app/data/models/penitipan_bantuan_model.dart +++ b/lib/app/data/models/penitipan_bantuan_model.dart @@ -78,8 +78,8 @@ class PenitipanBantuanModel { donatur: json["donatur"] != null ? DonaturModel.fromJson(json["donatur"]) : null, - kategoriBantuan: json["kategori_bantuan"] != null - ? KategoriBantuanModel.fromJson(json["kategori_bantuan"]) + kategoriBantuan: json["stok_bantuan"] != null + ? KategoriBantuanModel.fromJson(json["stok_bantuan"]) : null, isUang: json["is_uang"] ?? false, ); diff --git a/lib/app/modules/donatur/bindings/donatur_binding.dart b/lib/app/modules/donatur/bindings/donatur_binding.dart new file mode 100644 index 0000000..92c01e8 --- /dev/null +++ b/lib/app/modules/donatur/bindings/donatur_binding.dart @@ -0,0 +1,20 @@ +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; + +class DonaturBinding extends Bindings { + @override + void dependencies() { + // Hapus controller lama jika sudah ada + if (Get.isRegistered( + tag: 'donatur_dashboard')) { + Get.delete(tag: 'donatur_dashboard'); + } + + // Pasang controller baru + Get.put( + DonaturDashboardController(), + permanent: true, + tag: 'donatur_dashboard', + ); + } +} diff --git a/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart b/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart new file mode 100644 index 0000000..45c9302 --- /dev/null +++ b/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart @@ -0,0 +1,457 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:penyaluran_app/app/data/models/donatur_model.dart'; +import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; +import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart'; +import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; +import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart'; +import 'package:penyaluran_app/app/data/models/user_model.dart'; +import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; +import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; +import 'package:penyaluran_app/app/services/supabase_service.dart'; +import 'package:penyaluran_app/app/routes/app_pages.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +import 'package:supabase_flutter/supabase_flutter.dart'; + +class DonaturDashboardController extends GetxController { + final AuthController _authController = Get.find(); + final SupabaseService _supabaseService = SupabaseService.to; + + final Rx currentUser = Rx(null); + + // Variabel untuk foto profil + final RxString fotoProfil = ''.obs; + + // Indeks tab yang aktif di bottom navigation bar + final RxInt activeTabIndex = 0.obs; + + // Data untuk skema bantuan tersedia + final RxList skemaBantuan = [].obs; + + // Data untuk jadwal penyaluran + final RxList jadwalPenyaluran = + [].obs; + + // Data untuk riwayat penitipan bantuan + final RxList penitipanBantuan = + [].obs; + + // Data untuk laporan penyaluran + final RxList laporanPenyaluran = + [].obs; + + // Data untuk stok bantuan yang tersedia + final RxList stokBantuan = [].obs; + + // Indikator loading + final RxBool isLoading = false.obs; + + // Jumlah notifikasi belum dibaca + final RxInt jumlahNotifikasiBelumDibaca = 0.obs; + + // Data untuk foto bantuan pada form penitipan + final RxList fotoBantuanPaths = [].obs; + final ImagePicker _imagePicker = ImagePicker(); + + // Getter untuk data user + BaseUserModel? get user => _authController.baseUser; + String get role => user?.role ?? 'DONATUR'; + String get nama { + // Gunakan namaLengkap dari roleData jika tersedia + if (_authController.isDonatur && _authController.roleData != null) { + return _authController.roleData.namaLengkap ?? + _authController.displayName; + } + // Gunakan displayName dari AuthController + return _authController.displayName; + } + + String? get desa => user?.desa?.nama; + + // Getter untuk alamat dan noHp + String? get alamat { + if (_authController.isDonatur && _authController.roleData != null) { + return (_authController.roleData as DonaturModel).alamat; + } + return null; + } + + String? get noHp { + if (_authController.isDonatur && _authController.roleData != null) { + return (_authController.roleData as DonaturModel).noHp; + } + return null; + } + + // Getter untuk jenis donatur + String? get jenis { + if (_authController.isDonatur && _authController.roleData != null) { + return (_authController.roleData as DonaturModel).jenis; + } + return null; + } + + // Getter untuk foto profil + String? get profilePhotoUrl { + // 1. Coba ambil dari fotoProfil yang sudah disimpan + if (fotoProfil.isNotEmpty) { + return fotoProfil.value; + } + + // 2. Coba ambil dari roleData jika merupakan DonaturModel + if (_authController.isDonatur && _authController.roleData != null) { + final donaturData = _authController.roleData as DonaturModel; + if (donaturData.fotoProfil != null && + donaturData.fotoProfil!.isNotEmpty) { + return donaturData.fotoProfil; + } + } + + // 3. Coba ambil dari userData.roleData.fotoProfil + final userData = _authController.userData; + if (userData != null && userData.roleData is DonaturModel) { + final donaturData = userData.roleData as DonaturModel; + if (donaturData.fotoProfil != null && + donaturData.fotoProfil!.isNotEmpty) { + return donaturData.fotoProfil; + } + } + + return null; + } + + @override + void onInit() { + super.onInit(); + fetchData(); + loadUserData(); + } + + @override + void onReady() { + super.onReady(); + // Perbarui data user dan foto profil saat halaman siap + loadUserData(); + } + + void loadUserData() { + currentUser.value = _authController.baseUser; + + if (_authController.userData != null) { + if (_authController.isDonatur) { + var donaturData = _authController.roleData; + + // Ambil foto profil dari donaturData jika ada + if (donaturData != null && + donaturData.fotoProfil != null && + donaturData.fotoProfil!.isNotEmpty) { + fotoProfil.value = donaturData.fotoProfil!; + } + } + } + + // Ambil foto profil dari database + _fetchProfilePhoto(); + } + + // Metode untuk mengambil foto profil + Future _fetchProfilePhoto() async { + try { + if (user?.id == null) return; + + final donaturData = await _supabaseService.client + .from('donatur') + .select('foto_profil') + .eq('id', user!.id) + .maybeSingle(); + + if (donaturData != null && donaturData['foto_profil'] != null) { + fotoProfil.value = donaturData['foto_profil']; + } + } catch (e) { + print('Error fetching profile photo: $e'); + } + } + + void fetchData() async { + isLoading.value = true; + + try { + // Pastikan user sudah login dan memiliki ID + if (user?.id == null) { + throw Exception('User tidak terautentikasi'); + } + + // Ambil data skema bantuan + await fetchSkemaBantuan(); + + // Ambil data jadwal penyaluran + await fetchJadwalPenyaluran(); + + // Ambil data penitipan bantuan + await fetchPenitipanBantuan(); + + // Ambil data laporan penyaluran + await fetchLaporanPenyaluran(); + + // Ambil data stok bantuan + await fetchStokBantuan(); + + // Ambil data notifikasi + await fetchNotifikasi(); + } catch (e) { + print('Error fetching data: $e'); + } finally { + isLoading.value = false; + } + } + + // Ambil data skema bantuan + Future fetchSkemaBantuan() async { + try { + final response = await _supabaseService.client + .from('xx02_skema_bantuan') + .select() + .order('created_at', ascending: false); + + skemaBantuan.value = response + .map((data) => SkemaBantuanModel.fromJson(data)) + .toList() + .cast(); + } catch (e) { + print('Error fetching skema bantuan: $e'); + } + } + + // Ambil data jadwal penyaluran + Future fetchJadwalPenyaluran() async { + try { + final now = DateTime.now(); + final response = await _supabaseService.client + .from('penyaluran_bantuan') + .select() + .order('tanggal_penyaluran', ascending: true); + + // Konversi ke model lalu filter di sisi client + final allJadwal = response + .map((data) => PenyaluranBantuanModel.fromJson(data)) + .toList() + .cast(); + + // Filter jadwal yang tanggalnya lebih besar dari hari ini + jadwalPenyaluran.value = allJadwal + .where((jadwal) => + jadwal.tanggalPenyaluran != null && + jadwal.tanggalPenyaluran!.isAfter(now)) + .toList(); + } catch (e) { + print('Error fetching jadwal penyaluran: $e'); + } + } + + // Ambil data penitipan bantuan + Future fetchPenitipanBantuan() async { + try { + if (user?.id == null) return; + + final response = await _supabaseService.client + .from('penitipan_bantuan') + .select('*, donatur(*), stok_bantuan:stok_bantuan_id(*)') + .eq('donatur_id', user!.id) + .order('created_at', ascending: false); + + penitipanBantuan.value = response + .map((data) => PenitipanBantuanModel.fromJson(data)) + .toList() + .cast(); + } catch (e) { + print('Error fetching penitipan bantuan: $e'); + } + } + + // Ambil data laporan penyaluran + Future fetchLaporanPenyaluran() async { + try { + final response = await _supabaseService.client + .from('laporan_penyaluran') + .select() + .order('created_at', ascending: false); + + laporanPenyaluran.value = response + .map((data) => LaporanPenyaluranModel.fromJson(data)) + .toList() + .cast(); + } catch (e) { + print('Error fetching laporan penyaluran: $e'); + } + } + + // Ambil data stok bantuan + Future fetchStokBantuan() async { + try { + final response = await _supabaseService.client + .from('stok_bantuan') + .select('*, kategori_bantuan:kategori_bantuan_id(*)') + .order('nama', ascending: true); + + stokBantuan.value = response + .map((data) => StokBantuanModel.fromJson(data)) + .toList() + .cast(); + } catch (e) { + print('Error fetching stok bantuan: $e'); + } + } + + // Ambil data notifikasi + Future fetchNotifikasi() async { + try { + if (user?.id == null) return; + + final response = await _supabaseService.client + .from('notifikasi') + .select('*') + .eq('user_id', user!.id) + .eq('is_read', false) + .count(); + + jumlahNotifikasiBelumDibaca.value = response.count ?? 0; + } catch (e) { + print('Error fetching notifikasi: $e'); + } + } + + // Fungsi untuk logout + void logout() async { + try { + await _authController.logout(); + Get.offAllNamed(Routes.login); + } catch (e) { + print('Error during logout: $e'); + Get.snackbar( + 'Error', + 'Terjadi kesalahan saat logout: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + // Mendapatkan daftar stok bantuan yang tersedia + List getAvailableStokBantuan() { + // Filter stok bantuan yang jumlahnya lebih dari 0 + return stokBantuan.where((stok) => (stok.totalStok ?? 0) > 0).toList(); + } + + // Ambil gambar dari kamera atau galeri + Future pickImage({required bool isCamera}) async { + try { + final source = isCamera ? ImageSource.camera : ImageSource.gallery; + final pickedFile = await _imagePicker.pickImage( + source: source, + imageQuality: 70, // Kurangi kualitas untuk menghemat ukuran + ); + + if (pickedFile != null) { + fotoBantuanPaths.add(pickedFile.path); + } + } catch (e) { + print('Error picking image: $e'); + Get.snackbar( + 'Gagal', + 'Terjadi kesalahan saat mengambil gambar', + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Hapus foto bantuan dari daftar + void removeFotoBantuan(int index) { + if (index >= 0 && index < fotoBantuanPaths.length) { + fotoBantuanPaths.removeAt(index); + } + } + + // Reset foto bantuan paths + void resetFotoBantuan() { + fotoBantuanPaths.clear(); + } + + // Membuat penitipan bantuan baru + Future createPenitipanBantuan( + String? stokBantuanId, + double jumlah, + String deskripsi, + String? skemaBantuanId, + ) async { + try { + isLoading.value = true; + + if (user?.id == null) { + throw Exception('User tidak terautentikasi'); + } + + if (stokBantuanId == null) { + throw Exception('Stok bantuan harus dipilih'); + } + + if (fotoBantuanPaths.isEmpty) { + throw Exception('Foto bantuan harus diunggah'); + } + + // Unggah foto bantuan ke storage menggunakan metode dari SupabaseService + final fotoBantuanUrls = await _supabaseService.uploadMultipleFiles( + fotoBantuanPaths, 'penitipan', 'foto_bantuan'); + + // Data yang akan disimpan + final Map data = { + 'donatur_id': user!.id, + 'stok_bantuan_id': stokBantuanId, + 'jumlah': jumlah, + 'deskripsi': deskripsi, + 'status': 'MENUNGGU', + 'tanggal_penitipan': DateTime.now().toIso8601String(), + 'foto_bantuan': fotoBantuanUrls, + }; + + // Tambahkan skema bantuan jika ada + if (skemaBantuanId != null && skemaBantuanId.isNotEmpty) { + data['skema_bantuan_id'] = skemaBantuanId; + } + + // Simpan ke database + await _supabaseService.client.from('penitipan_bantuan').insert(data); + + // Reset foto bantuan setelah berhasil disimpan + resetFotoBantuan(); + + // Ambil data penitipan bantuan yang baru + await fetchPenitipanBantuan(); + + // Tampilkan pesan sukses + Get.snackbar( + 'Berhasil', + 'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + + // Pindah ke tab riwayat penitipan + DefaultTabController.of(Get.context!)?.animateTo(0); + } catch (e) { + print('Error creating penitipan bantuan: $e'); + Get.snackbar( + 'Gagal', + 'Terjadi kesalahan saat mengirim penitipan bantuan: $e', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/app/modules/donatur/views/donatur_dashboard_view.dart b/lib/app/modules/donatur/views/donatur_dashboard_view.dart new file mode 100644 index 0000000..2353085 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_dashboard_view.dart @@ -0,0 +1,774 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/routes/app_pages.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; + +class DonaturDashboardView extends GetView { + const DonaturDashboardView({super.key}); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return RefreshIndicator( + onRefresh: () async { + controller.fetchData(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeSection(), + const SizedBox(height: 24), + _buildStatisticSection(), + const SizedBox(height: 24), + _buildUpcomingEvents(), + const SizedBox(height: 24), + _buildRecentPenitipan(), + ], + ), + ), + ); + }), + ); + } + + Widget _buildWelcomeSection() { + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [Colors.white, Colors.blue.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.blue.shade200, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Hero( + tag: 'donatur-profile', + child: CircleAvatar( + radius: 30, + backgroundColor: Colors.blue.shade100, + backgroundImage: controller.profilePhotoUrl != null + ? NetworkImage(controller.profilePhotoUrl!) + : null, + child: controller.profilePhotoUrl == null + ? Icon( + Icons.person, + color: Colors.blue.shade700, + size: 30, + ) + : null, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat Datang,', + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade700, + fontWeight: FontWeight.w500, + ), + ), + Text( + controller.nama, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue.shade900, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + _buildInfoRow( + icon: Icons.home_rounded, + iconColor: Colors.blue.shade300, + label: 'Alamat', + value: controller.alamat ?? 'Alamat tidak tersedia', + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1), + ), + _buildInfoRow( + icon: Icons.phone_rounded, + iconColor: Colors.green.shade300, + label: 'No. HP', + value: controller.noHp ?? 'No. HP tidak tersedia', + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1), + ), + _buildInfoRow( + icon: Icons.category_rounded, + iconColor: Colors.amber.shade300, + label: 'Jenis Donatur', + value: controller.jenis ?? 'Jenis tidak tersedia', + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildActionButton( + icon: Icons.edit_rounded, + label: 'Edit Profil', + color: Colors.blue.shade700, + onTap: () => Get.toNamed(Routes.profile), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildActionButton( + icon: Icons.add_box_rounded, + label: 'Titip Bantuan', + color: Colors.green.shade700, + onTap: () { + // Navigasi ke form penitipan bantuan + controller.activeTabIndex.value = 3; + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatisticSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Statistik Kontribusi', + ), + Text( + 'Ringkasan aktivitas anda sebagai donatur', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildStatCard( + title: 'Total Bantuan', + value: '${controller.penitipanBantuan.length}', + icon: Icons.card_giftcard, + color: Colors.blue, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + title: 'Menunggu Verifikasi', + value: + '${controller.penitipanBantuan.where((p) => p.status == 'MENUNGGU').length}', + icon: Icons.hourglass_empty, + color: Colors.orange, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildStatCard( + title: 'Diterima', + value: + '${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}', + icon: Icons.check_circle_outline, + color: Colors.green, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + title: 'Ditolak', + value: + '${controller.penitipanBantuan.where((p) => p.status == 'DITOLAK').length}', + icon: Icons.cancel_outlined, + color: Colors.red, + ), + ), + ], + ), + ], + ); + } + + Widget _buildUpcomingEvents() { + final upcomingEvents = controller.jadwalPenyaluran + .where((event) => + event.tanggalPenyaluran != null && + event.tanggalPenyaluran!.isAfter(DateTime.now())) + .take(3) + .toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Jadwal Penyaluran', + ), + Text( + 'Jadwal penyaluran bantuan terdekat', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + TextButton( + onPressed: () { + // Navigasi ke tab jadwal penyaluran + controller.activeTabIndex.value = 2; + }, + child: Text( + 'Lihat Semua', + style: TextStyle(color: Colors.blue.shade700), + ), + ), + ], + ), + const SizedBox(height: 8), + if (upcomingEvents.isEmpty) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Text( + 'Tidak ada jadwal penyaluran dalam waktu dekat', + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + ...upcomingEvents.map((event) => _buildEventCard(event)), + ], + ); + } + + Widget _buildRecentPenitipan() { + final recentPenitipan = controller.penitipanBantuan.take(3).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Bantuan Terakhir', + ), + Text( + 'Riwayat penitipan bantuan terakhir', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + TextButton( + onPressed: () { + // Navigasi ke tab riwayat penitipan + controller.activeTabIndex.value = 3; + }, + child: Text( + 'Lihat Semua', + style: TextStyle(color: Colors.blue.shade700), + ), + ), + ], + ), + const SizedBox(height: 8), + if (recentPenitipan.isEmpty) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Text( + 'Belum ada riwayat penitipan bantuan', + style: TextStyle(color: Colors.grey), + ), + ), + ) + else + ...recentPenitipan.map((penitipan) => _buildPenitipanCard(penitipan)), + ], + ); + } + + Widget _buildInfoRow({ + required IconData icon, + required Color iconColor, + required String label, + required String value, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + size: 16, + color: iconColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: color, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16), + const SizedBox(width: 8), + Text(label), + ], + ), + ); + } + + Widget _buildStatCard({ + required String title, + required String value, + required IconData icon, + required Color color, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 24, + ), + ), + const SizedBox(height: 12), + Text( + title, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildEventCard(dynamic event) { + final formattedDate = event.tanggalPenyaluran != null + ? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!) + : 'Tanggal tidak tersedia'; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event, + color: Colors.blue.shade700, + size: 24, + ), + Text( + event.tanggalPenyaluran != null + ? DateFormat('dd').format(event.tanggalPenyaluran!) + : '--', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + event.nama ?? 'Penyaluran Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + event.deskripsi ?? 'Tidak ada deskripsi', + style: const TextStyle(fontSize: 14), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPenitipanCard(dynamic penitipan) { + final formattedDate = penitipan.tanggalPenitipan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(penitipan.tanggalPenitipan!) + : 'Tanggal tidak tersedia'; + + Color statusColor; + IconData statusIcon; + + switch (penitipan.status) { + case 'DITERIMA': + statusColor = Colors.green; + statusIcon = Icons.check_circle; + break; + case 'DITOLAK': + statusColor = Colors.red; + statusIcon = Icons.cancel; + break; + case 'MENUNGGU': + default: + statusColor = Colors.orange; + statusIcon = Icons.hourglass_empty; + break; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + statusIcon, + color: statusColor, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + penitipan.kategoriBantuan?.nama ?? 'Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + penitipan.status ?? 'MENUNGGU', + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.inventory_2_outlined, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + 'Jumlah: ${penitipan.jumlah ?? 0}', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_jadwal_view.dart b/lib/app/modules/donatur/views/donatur_jadwal_view.dart new file mode 100644 index 0000000..94720b0 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_jadwal_view.dart @@ -0,0 +1,322 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; + +class DonaturJadwalView extends GetView { + const DonaturJadwalView({super.key}); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchJadwalPenyaluran(); + }, + child: controller.jadwalPenyaluran.isEmpty + ? _buildEmptyState() + : _buildJadwalList(), + ); + }), + ); + } + + Widget _buildEmptyState() { + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event_busy, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text( + 'Belum Ada Jadwal Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Jadwal penyaluran bantuan belum tersedia saat ini', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => controller.fetchJadwalPenyaluran(), + icon: const Icon(Icons.refresh), + label: const Text('Muat Ulang'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildJadwalList() { + // Kelompokkan jadwal berdasarkan bulan + Map> groupedJadwal = {}; + + for (var jadwal in controller.jadwalPenyaluran) { + if (jadwal.tanggalPenyaluran != null) { + String monthYear = + DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!); + + if (!groupedJadwal.containsKey(monthYear)) { + groupedJadwal[monthYear] = []; + } + + groupedJadwal[monthYear]!.add(jadwal); + } + } + + // Urutkan kunci (bulan) secara kronologis + List sortedMonths = groupedJadwal.keys.toList() + ..sort((a, b) { + DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a); + DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b); + return dateA.compareTo(dateB); + }); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + const SectionHeader(title: 'Jadwal Penyaluran Bantuan'), + Text( + 'Daftar jadwal penyaluran bantuan yang akan datang', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + + // Tampilkan jadwal berdasarkan bulan + ...sortedMonths + .map((month) => _buildMonthSection(month, groupedJadwal[month]!)), + ], + ); + } + + Widget _buildMonthSection(String month, List jadwalList) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + month, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ), + ...jadwalList.map((jadwal) => _buildJadwalCard(jadwal)), + const SizedBox(height: 8), + ], + ); + } + + Widget _buildJadwalCard(dynamic jadwal) { + final formattedDate = jadwal.tanggalPenyaluran != null + ? DateFormat('EEEE, dd MMMM yyyy', 'id_ID') + .format(jadwal.tanggalPenyaluran!) + : 'Tanggal tidak tersedia'; + + String statusText = 'Akan Datang'; + Color statusColor = Colors.blue; + + switch (jadwal.status) { + case 'SELESAI': + statusText = 'Selesai'; + statusColor = Colors.green; + break; + case 'DIBATALKAN': + statusText = 'Dibatalkan'; + statusColor = Colors.red; + break; + case 'DALAM_PROSES': + statusText = 'Dalam Proses'; + statusColor = Colors.orange; + break; + default: + statusText = 'Akan Datang'; + statusColor = Colors.blue; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (jadwal.tanggalPenyaluran != null) ...[ + Text( + DateFormat('dd').format(jadwal.tanggalPenyaluran!), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + Text( + DateFormat('MMM', 'id_ID') + .format(jadwal.tanggalPenyaluran!) + .toUpperCase(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ] else + Icon( + Icons.event, + color: statusColor, + size: 24, + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + jadwal.nama ?? 'Penyaluran Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + statusText, + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + if (jadwal.deskripsi != null && jadwal.deskripsi!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + jadwal.deskripsi!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_laporan_view.dart b/lib/app/modules/donatur/views/donatur_laporan_view.dart new file mode 100644 index 0000000..647eb09 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_laporan_view.dart @@ -0,0 +1,409 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; + +class DonaturLaporanView extends GetView { + const DonaturLaporanView({super.key}); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchLaporanPenyaluran(); + }, + child: controller.laporanPenyaluran.isEmpty + ? _buildEmptyState() + : _buildLaporanList(), + ); + }), + ); + } + + Widget _buildEmptyState() { + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.assignment_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text( + 'Belum Ada Laporan Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Laporan penyaluran bantuan belum tersedia', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => controller.fetchLaporanPenyaluran(), + icon: const Icon(Icons.refresh), + label: const Text('Muat Ulang'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildLaporanList() { + // Urutkan laporan berdasarkan tanggal, yang terbaru di atas + final sortedLaporan = controller.laporanPenyaluran.toList() + ..sort((a, b) { + if (a.tanggalLaporan == null || b.tanggalLaporan == null) { + return 0; + } + return b.tanggalLaporan!.compareTo(a.tanggalLaporan!); + }); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + const SectionHeader(title: 'Laporan Penyaluran Bantuan'), + Text( + 'Daftar laporan hasil penyaluran bantuan', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + ...sortedLaporan.map((laporan) => _buildLaporanCard(laporan)), + ], + ); + } + + Widget _buildLaporanCard(dynamic laporan) { + final formattedDate = laporan.tanggalLaporan != null + ? DateFormat('dd MMMM yyyy', 'id_ID').format(laporan.tanggalLaporan!) + : 'Tanggal tidak tersedia'; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: laporan.dokumentasiUrl != null && + laporan.dokumentasiUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + laporan.dokumentasiUrl!, + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.assignment, + color: Colors.blue.shade700, + size: 30, + ); + }, + ), + ) + : Icon( + Icons.assignment, + color: Colors.blue.shade700, + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + laporan.judul, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + if (laporan.beritaAcaraUrl != null && + laporan.beritaAcaraUrl!.isNotEmpty) + ElevatedButton.icon( + onPressed: () { + // Implementasi untuk membuka berita acara + _openDocument(laporan.beritaAcaraUrl!); + }, + icon: const Icon(Icons.description, size: 16), + label: const Text('Berita Acara'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: const Size(30, 30), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + textStyle: const TextStyle(fontSize: 12), + ), + ), + if (laporan.dokumentasiUrl != null && + laporan.dokumentasiUrl!.isNotEmpty) ...[ + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + // Implementasi untuk melihat dokumentasi + _viewDocumentation(laporan.dokumentasiUrl!); + }, + icon: const Icon(Icons.photo_library, size: 16), + label: const Text('Dokumentasi'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: const Size(30, 30), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + textStyle: const TextStyle(fontSize: 12), + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + void _openDocument(String url) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Buka Dokumen', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + const Text( + 'Anda akan membuka dokumen berita acara. Lanjutkan?', + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + // Implementasi untuk membuka URL dokumen + // Misalnya menggunakan package url_launcher + // launch(url); + Get.snackbar( + 'Info', + 'Membuka dokumen: $url', + snackPosition: SnackPosition.BOTTOM, + ); + }, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + ), + child: const Text('Buka'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + void _viewDocumentation(String url) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + insetPadding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Dokumentasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ], + ), + ), + Container( + constraints: BoxConstraints( + maxHeight: Get.height * 0.6, + maxWidth: Get.width, + ), + child: Image.network( + url, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 50, + color: Colors.red.shade300, + ), + const SizedBox(height: 16), + const Text('Gagal memuat gambar'), + ], + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_penitipan_view.dart b/lib/app/modules/donatur/views/donatur_penitipan_view.dart new file mode 100644 index 0000000..f3cb54d --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_penitipan_view.dart @@ -0,0 +1,1150 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; +import 'dart:io'; + +class DonaturPenitipanView extends GetView { + const DonaturPenitipanView({super.key}); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return FormPenitipanBantuan(); + } +} + +class DonaturRiwayatPenitipanView extends GetView { + DonaturRiwayatPenitipanView({Key? key}) : super(key: key); + + @override + DonaturDashboardController get controller { + return Get.find(tag: 'donatur_dashboard'); + } + + final TextEditingController searchController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + appBar: AppBar( + title: const Text('Riwayat Penitipan'), + bottom: const TabBar( + tabs: [ + Tab(text: 'Menunggu'), + Tab(text: 'Diterima'), + Tab(text: 'Ditolak'), + ], + ), + ), + body: TabBarView( + children: [ + // Tab Menunggu + _buildPenitipanList(context, 'MENUNGGU'), + // Tab Diterima + _buildPenitipanList(context, 'DITERIMA'), + // Tab Ditolak + _buildPenitipanList(context, 'DITOLAK'), + ], + ), + ), + ); + } + + Widget _buildPenitipanList(BuildContext context, String status) { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + // Filter penitipan berdasarkan status + var filteredList = controller.penitipanBantuan + .where((item) => item.status == status) + .toList(); + + // Filter berdasarkan pencarian + final searchText = searchController.text.toLowerCase(); + if (searchText.isNotEmpty) { + filteredList = filteredList.where((item) { + final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? ''; + final deskripsi = item.deskripsi?.toLowerCase() ?? ''; + final tanggal = item.tanggalPenitipan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(item.tanggalPenitipan!) + .toLowerCase() + : ''; + + return kategoriNama.contains(searchText) || + deskripsi.contains(searchText) || + tanggal.contains(searchText); + }).toList(); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchPenitipanBantuan(); + }, + child: filteredList.isEmpty + ? _buildEmptyState(status) + : _buildContentList(context, filteredList, status), + ); + }); + } + + Widget _buildEmptyState(String status) { + String statusText = ''; + switch (status) { + case 'MENUNGGU': + statusText = 'menunggu verifikasi'; + break; + case 'DITERIMA': + statusText = 'diterima'; + break; + case 'DITOLAK': + statusText = 'ditolak'; + break; + } + + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Tidak ada penitipan $statusText', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Anda belum memiliki riwayat penitipan yang $statusText', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } + + Widget _buildContentList( + BuildContext context, List filteredList, String status) { + Color statusColor; + switch (status) { + case 'DITERIMA': + statusColor = Colors.green; + break; + case 'DITOLAK': + statusColor = Colors.red; + break; + case 'MENUNGGU': + default: + statusColor = Colors.orange; + break; + } + + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search field + TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Cari riwayat penitipan...', + 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) { + // Trigger update dengan GetX + controller.update(); + }, + ), + const SizedBox(height: 16), + // Info jumlah item + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Daftar Penitipan ${status.toLowerCase()}', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + '${filteredList.length} item', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + const SizedBox(height: 16), + // Daftar penitipan + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: filteredList.length, + itemBuilder: (context, index) { + return _buildPenitipanCard( + context, filteredList[index], statusColor); + }, + ), + ], + ), + ), + ); + } + + Widget _buildPenitipanCard( + BuildContext context, dynamic penitipan, Color statusColor) { + final formattedDate = penitipan.tanggalPenitipan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(penitipan.tanggalPenitipan!) + : 'Tanggal tidak tersedia'; + + IconData statusIcon; + + switch (penitipan.status) { + case 'DITERIMA': + statusIcon = Icons.check_circle; + break; + case 'DITOLAK': + statusIcon = Icons.cancel; + break; + case 'MENUNGGU': + default: + statusIcon = Icons.hourglass_empty; + break; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + statusIcon, + color: statusColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + penitipan.kategoriBantuan?.nama ?? 'Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + penitipan.status ?? 'MENUNGGU', + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.inventory_2_outlined, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + 'Jumlah: ${penitipan.jumlah ?? 0}', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + ), + ], + ), + if (penitipan.deskripsi != null && + penitipan.deskripsi!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + penitipan.deskripsi!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + if (penitipan.status == 'DITOLAK' && + penitipan.alasanPenolakan != null && + penitipan.alasanPenolakan!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + 'Alasan Penolakan:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.red.shade700, + ), + ), + const SizedBox(height: 4), + Text( + penitipan.alasanPenolakan!, + style: TextStyle( + fontSize: 14, + color: Colors.red.shade700, + ), + ), + ], + ], + ), + ), + ), + ); + } + + Widget _buildContactInfo({ + required IconData icon, + required String title, + required String content, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: Colors.blue, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + content, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ); + } +} + +class FormPenitipanBantuan extends StatefulWidget { + @override + _FormPenitipanBantuanState createState() => _FormPenitipanBantuanState(); +} + +class _FormPenitipanBantuanState extends State { + final DonaturDashboardController controller = + Get.find(tag: 'donatur_dashboard'); + + final GlobalKey formKey = GlobalKey(); + String? selectedStokBantuanId; + String? selectedSkemaBantuanId; + final TextEditingController jumlahController = TextEditingController(); + final TextEditingController deskripsiController = TextEditingController(); + + @override + void initState() { + super.initState(); + // Reset foto bantuan saat form dibuka + controller.resetFotoBantuan(); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return Form( + key: formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Formulir Penitipan Bantuan'), + Text( + 'Isi formulir berikut untuk melakukan penitipan bantuan', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 24), + + // Pilih metode penitipan + Text( + 'Metode Penitipan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + + // Tab pilihan metode + Container( + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + setState(() { + selectedSkemaBantuanId = null; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: selectedSkemaBantuanId == null + ? Colors.blue + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + 'Bantuan Manual', + style: TextStyle( + fontWeight: FontWeight.bold, + color: selectedSkemaBantuanId == null + ? Colors.white + : Colors.grey.shade800, + ), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: () { + setState(() { + // Reset stok bantuan saat memilih skema + selectedStokBantuanId = null; + selectedSkemaBantuanId = ''; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: selectedSkemaBantuanId != null + ? Colors.blue + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + 'Dari Skema Bantuan', + style: TextStyle( + fontWeight: FontWeight.bold, + color: selectedSkemaBantuanId != null + ? Colors.white + : Colors.grey.shade800, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Form berdasarkan pilihan + if (selectedSkemaBantuanId != null) ...[ + // Form untuk skema bantuan + Text( + 'Pilih Skema Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + hintText: 'Pilih skema bantuan', + ), + value: selectedSkemaBantuanId == '' + ? null + : selectedSkemaBantuanId, + items: controller.skemaBantuan.map((skema) { + return DropdownMenuItem( + value: skema.id, + child: Text(skema.nama ?? 'Tidak ada nama'), + ); + }).toList(), + onChanged: (value) { + setState(() { + selectedSkemaBantuanId = value; + // Jika skema dipilih, isi otomatis stok bantuan sesuai dengan skema + if (value != null) { + final selectedSkema = + controller.skemaBantuan.firstWhere( + (skema) => skema.id == value, + orElse: () => SkemaBantuanModel(), + ); + selectedStokBantuanId = selectedSkema.stokBantuanId; + + // Isi otomatis jumlah jika ada + if (selectedSkema.jumlahDiterimaPerOrang != null) { + jumlahController.text = + selectedSkema.jumlahDiterimaPerOrang.toString(); + } + } + }); + }, + validator: (value) { + if (selectedSkemaBantuanId != null && + (value == null || value.isEmpty)) { + return 'Skema bantuan harus dipilih'; + } + return null; + }, + ), + const SizedBox(height: 16), + ] else ...[ + // Form untuk bantuan manual + Text( + 'Jenis Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + hintText: 'Pilih jenis bantuan', + ), + value: selectedStokBantuanId, + items: controller.getAvailableStokBantuan().map((stok) { + return DropdownMenuItem( + value: stok.id, + child: Text( + '${stok.nama ?? 'Tidak ada nama'} (Stok: ${stok.totalStok ?? 0} ${stok.satuan ?? 'item'})'), + ); + }).toList(), + onChanged: (value) { + setState(() { + selectedStokBantuanId = value; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Jenis bantuan harus dipilih'; + } + return null; + }, + ), + const SizedBox(height: 16), + ], + + // Jumlah bantuan + Text( + 'Jumlah Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: jumlahController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + hintText: 'Masukkan jumlah bantuan', + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Jumlah harus diisi'; + } + if (double.tryParse(value) == null) { + return 'Jumlah harus berupa angka'; + } + if (double.parse(value) <= 0) { + return 'Jumlah harus lebih dari 0'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Deskripsi bantuan + Text( + 'Deskripsi Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: deskripsiController, + maxLines: 3, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + hintText: 'Deskripsi bantuan yang dititipkan', + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Deskripsi harus diisi'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Foto bantuan + Text( + 'Foto Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + + // Widget untuk foto bantuan + Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tampilkan foto yang sudah dipilih + if (controller.fotoBantuanPaths.isNotEmpty) ...[ + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: controller.fotoBantuanPaths.length + + 1, // +1 untuk tombol tambah + itemBuilder: (context, index) { + if (index == controller.fotoBantuanPaths.length) { + // Tombol tambah foto + return GestureDetector( + onTap: _showPilihSumberFoto, + child: Container( + width: 120, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade400), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo, + size: 32, + color: Colors.grey.shade600, + ), + const SizedBox(height: 4), + Text( + 'Tambah Foto', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } + + // Tampilkan foto yang sudah dipilih + return Stack( + children: [ + Container( + width: 120, + height: 120, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade400), + image: DecorationImage( + image: FileImage(File(controller + .fotoBantuanPaths[index])), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 4, + right: 12, + child: GestureDetector( + onTap: () { + controller.removeFotoBantuan(index); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.7), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + size: 18, + color: Colors.red, + ), + ), + ), + ), + ], + ); + }, + ), + ), + ] else ...[ + // Tampilkan placeholder untuk upload foto + GestureDetector( + onTap: _showPilihSumberFoto, + child: Container( + height: 120, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade400), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo, + size: 40, + color: Colors.grey.shade600, + ), + const SizedBox(height: 8), + Text( + 'Tambah Foto Bantuan', + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Upload minimal 1 foto bantuan', + style: TextStyle( + color: Colors.grey.shade500, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ], + ], + )), + + const SizedBox(height: 24), + + // Tombol kirim + ElevatedButton.icon( + onPressed: () { + if (formKey.currentState!.validate()) { + // Validasi foto bantuan + if (controller.fotoBantuanPaths.isEmpty) { + Get.snackbar( + 'Peringatan', + 'Harap upload setidaknya 1 foto bantuan', + backgroundColor: Colors.amber, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + return; + } + + // Tampilkan konfirmasi sebelum mengirim + Get.dialog( + AlertDialog( + title: const Text('Konfirmasi Penitipan Bantuan'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Apakah data yang Anda masukkan sudah benar?'), + const SizedBox(height: 12), + const Text( + 'Penitipan bantuan akan diproses oleh petugas desa.'), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + // Panggil fungsi untuk membuat penitipan bantuan + controller.createPenitipanBantuan( + selectedStokBantuanId, + double.parse(jumlahController.text), + deskripsiController.text, + selectedSkemaBantuanId, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: const Text('Kirim'), + ), + ], + ), + ); + } + }, + icon: const Icon(Icons.send), + label: const Text('Kirim Penitipan Bantuan'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 45), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 16), + + // Informasi kontak petugas + Text( + 'Hubungi Petugas Desa', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Text( + 'Untuk penitipan bantuan secara langsung, silahkan hubungi petugas desa terdekat atau kunjungi kantor desa terdekat.', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: () { + // Implementasi untuk membuka kontak petugas desa + Get.dialog( + AlertDialog( + title: const Text('Informasi Kontak Petugas Desa'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildContactInfo( + icon: Icons.phone, + title: 'Telepon', + content: '0812-3456-7890', + ), + const SizedBox(height: 16), + _buildContactInfo( + icon: Icons.email, + title: 'Email', + content: 'petugas@desa.id', + ), + const SizedBox(height: 16), + _buildContactInfo( + icon: Icons.location_on, + title: 'Alamat', + content: + 'Jl. Desa Sejahtera No. 123, Kecamatan Makmur', + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Tutup'), + ), + ], + ), + ); + }, + icon: const Icon(Icons.contact_phone), + label: const Text('Lihat Kontak Petugas Desa'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.blue, + side: BorderSide(color: Colors.blue.shade300), + minimumSize: const Size(double.infinity, 45), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ); + }); + } + + // Fungsi untuk memilih foto + void _showPilihSumberFoto() { + Get.bottomSheet( + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Pilih Sumber Foto', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text('Kamera'), + onTap: () { + Get.back(); + controller.pickImage(isCamera: true); + }, + ), + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text('Galeri'), + onTap: () { + Get.back(); + controller.pickImage(isCamera: false); + }, + ), + ], + ), + ), + ); + } + + Widget _buildContactInfo({ + required IconData icon, + required String title, + required String content, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: Colors.blue, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + content, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_riwayat_penitipan_view.dart b/lib/app/modules/donatur/views/donatur_riwayat_penitipan_view.dart new file mode 100644 index 0000000..f32ceda --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_riwayat_penitipan_view.dart @@ -0,0 +1,393 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; + +class DonaturRiwayatPenitipanView extends GetView { + DonaturRiwayatPenitipanView({Key? key}) : super(key: key); + + @override + DonaturDashboardController get controller { + return Get.find(tag: 'donatur_dashboard'); + } + + final TextEditingController searchController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + appBar: AppBar( + title: const Text('Riwayat Penitipan'), + bottom: const TabBar( + tabs: [ + Tab(text: 'Menunggu'), + Tab(text: 'Diterima'), + Tab(text: 'Ditolak'), + ], + ), + ), + body: TabBarView( + children: [ + // Tab Menunggu + _buildPenitipanList(context, 'MENUNGGU'), + // Tab Diterima + _buildPenitipanList(context, 'DITERIMA'), + // Tab Ditolak + _buildPenitipanList(context, 'DITOLAK'), + ], + ), + ), + ); + } + + Widget _buildPenitipanList(BuildContext context, String status) { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + // Filter penitipan berdasarkan status + var filteredList = controller.penitipanBantuan + .where((item) => item.status == status) + .toList(); + + // Filter berdasarkan pencarian + final searchText = searchController.text.toLowerCase(); + if (searchText.isNotEmpty) { + filteredList = filteredList.where((item) { + final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? ''; + final deskripsi = item.deskripsi?.toLowerCase() ?? ''; + final tanggal = item.tanggalPenitipan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(item.tanggalPenitipan!) + .toLowerCase() + : ''; + + return kategoriNama.contains(searchText) || + deskripsi.contains(searchText) || + tanggal.contains(searchText); + }).toList(); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchPenitipanBantuan(); + }, + child: filteredList.isEmpty + ? _buildEmptyState(status) + : _buildContentList(context, filteredList, status), + ); + }); + } + + Widget _buildEmptyState(String status) { + String statusText = ''; + switch (status) { + case 'MENUNGGU': + statusText = 'menunggu verifikasi'; + break; + case 'DITERIMA': + statusText = 'diterima'; + break; + case 'DITOLAK': + statusText = 'ditolak'; + break; + } + + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Tidak ada penitipan $statusText', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Anda belum memiliki riwayat penitipan yang $statusText', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } + + Widget _buildContentList( + BuildContext context, List filteredList, String status) { + Color statusColor; + switch (status) { + case 'DITERIMA': + statusColor = Colors.green; + break; + case 'DITOLAK': + statusColor = Colors.red; + break; + case 'MENUNGGU': + default: + statusColor = Colors.orange; + break; + } + + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search field + TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Cari riwayat penitipan...', + 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) { + // Trigger update dengan GetX + controller.update(); + }, + ), + const SizedBox(height: 16), + // Info jumlah item + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Daftar Penitipan ${status.toLowerCase()}', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + '${filteredList.length} item', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + const SizedBox(height: 16), + // Daftar penitipan + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: filteredList.length, + itemBuilder: (context, index) { + return _buildPenitipanCard( + context, filteredList[index], statusColor); + }, + ), + ], + ), + ), + ); + } + + Widget _buildPenitipanCard( + BuildContext context, dynamic penitipan, Color statusColor) { + final formattedDate = penitipan.tanggalPenitipan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(penitipan.tanggalPenitipan!) + : 'Tanggal tidak tersedia'; + + IconData statusIcon; + + switch (penitipan.status) { + case 'DITERIMA': + statusIcon = Icons.check_circle; + break; + case 'DITOLAK': + statusIcon = Icons.cancel; + break; + case 'MENUNGGU': + default: + statusIcon = Icons.hourglass_empty; + break; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + statusIcon, + color: statusColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + penitipan.kategoriBantuan?.nama ?? 'Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + penitipan.status ?? 'MENUNGGU', + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.inventory_2_outlined, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + 'Jumlah: ${penitipan.jumlah ?? 0}', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + ), + ], + ), + if (penitipan.deskripsi != null && + penitipan.deskripsi!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + penitipan.deskripsi!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + if (penitipan.status == 'DITOLAK' && + penitipan.alasanPenolakan != null && + penitipan.alasanPenolakan!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + 'Alasan Penolakan:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.red.shade700, + ), + ), + const SizedBox(height: 4), + Text( + penitipan.alasanPenolakan!, + style: TextStyle( + fontSize: 14, + color: Colors.red.shade700, + ), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_skema_view.dart b/lib/app/modules/donatur/views/donatur_skema_view.dart new file mode 100644 index 0000000..5372011 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_skema_view.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; + +class DonaturSkemaView extends GetView { + const DonaturSkemaView({super.key}); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchSkemaBantuan(); + }, + child: controller.skemaBantuan.isEmpty + ? _buildEmptyState() + : _buildSkemaList(), + ); + }), + ); + } + + Widget _buildEmptyState() { + return Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.description_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text( + 'Belum Ada Skema Bantuan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Skema bantuan belum tersedia saat ini', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => controller.fetchSkemaBantuan(), + icon: const Icon(Icons.refresh), + label: const Text('Muat Ulang'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSkemaList() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + const SectionHeader(title: 'Skema Bantuan Tersedia'), + Text( + 'Daftar skema bantuan yang dapat Anda titipkan', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + ...controller.skemaBantuan.map((skema) => _buildSkemaCard(skema)), + ], + ); + } + + Widget _buildSkemaCard(dynamic skema) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.volunteer_activism, + color: Colors.blue.shade700, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + skema.nama ?? 'Skema Bantuan', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + if (skema.kuota != null) + _buildInfoChip( + icon: Icons.people_outline, + label: 'Kuota: ${skema.kuota} penerima', + color: Colors.blue.shade700, + ), + if (skema.jumlahDiterimaPerOrang != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: _buildInfoChip( + icon: Icons.inventory_2_outlined, + label: + 'Jumlah per orang: ${skema.jumlahDiterimaPerOrang}', + color: Colors.green.shade700, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + if (skema.deskripsi != null && skema.deskripsi!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Deskripsi', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Text( + skema.deskripsi!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const Divider(height: 24), + ], + ), + if (skema.kriteria != null && skema.kriteria!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Kriteria Penerima', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Text( + skema.kriteria!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 16), + ], + ), + ElevatedButton.icon( + onPressed: () { + // Navigasi ke formulir penitipan bantuan + controller.activeTabIndex.value = 3; + }, + icon: const Icon(Icons.add_box_outlined, size: 18), + label: const Text('Titipkan Bantuan'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.green.shade600, + minimumSize: const Size(double.infinity, 45), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoChip({ + required IconData icon, + required String label, + required Color color, + }) { + return Row( + children: [ + Icon( + icon, + size: 14, + color: color, + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/lib/app/modules/donatur/views/donatur_view.dart b/lib/app/modules/donatur/views/donatur_view.dart new file mode 100644 index 0000000..682a918 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_view.dart @@ -0,0 +1,326 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_dashboard_view.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_skema_view.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_jadwal_view.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_laporan_view.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_penitipan_view.dart'; +import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; + +class DonaturView extends GetView { + final GlobalKey scaffoldKey = GlobalKey(); + + DonaturView({super.key}); + + // Override untuk mendapatkan controller dengan tag + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: scaffoldKey, + appBar: AppBar( + title: Obx(() { + switch (controller.activeTabIndex.value) { + case 0: + return const Text('Dashboard Donatur'); + case 1: + return const Text('Skema Bantuan'); + case 2: + return const Text('Jadwal Penyaluran'); + case 3: + return const Text('Penitipan Bantuan'); + case 4: + return const Text('Laporan Penyaluran'); + default: + return const Text('Dashboard Donatur'); + } + }), + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + scaffoldKey.currentState?.openDrawer(); + }, + ), + actions: [ + // Tombol riwayat penitipan khusus untuk tab penitipan bantuan + Obx(() => controller.activeTabIndex.value == 3 + ? IconButton( + icon: const Icon(Icons.history), + onPressed: () { + // Navigasi ke halaman riwayat penitipan + Get.to( + () => DonaturRiwayatPenitipanView(), + transition: Transition.rightToLeft, + ); + }, + ) + : const SizedBox.shrink()), + // Tombol notifikasi + Stack( + alignment: Alignment.center, + children: [ + IconButton( + icon: const Icon(Icons.notifications_outlined), + onPressed: () { + // Navigasi ke halaman notifikasi + Get.toNamed('/notifikasi'); + }, + ), + Obx(() { + if (controller.jumlahNotifikasiBelumDibaca.value > 0) { + return Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Text( + controller.jumlahNotifikasiBelumDibaca.value.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + textAlign: TextAlign.center, + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ), + ], + ), + drawer: _buildDrawer(context), + body: Obx(() { + // Tampilkan sesuai dengan tab yang aktif + switch (controller.activeTabIndex.value) { + case 0: + return const DonaturDashboardView(); + case 1: + return const DonaturSkemaView(); + case 2: + return const DonaturJadwalView(); + case 3: + return const DonaturPenitipanView(); + case 4: + return const DonaturLaporanView(); + default: + return const DonaturDashboardView(); + } + }), + bottomNavigationBar: Obx(() => AppBottomNavigationBar( + currentIndex: controller.activeTabIndex.value, + onTap: (index) { + controller.activeTabIndex.value = index; + }, + items: [ + AppBottomNavigationBarItem( + icon: Icons.dashboard_outlined, + activeIcon: Icons.dashboard, + label: 'Dashboard', + ), + AppBottomNavigationBarItem( + icon: Icons.description_outlined, + activeIcon: Icons.description, + label: 'Skema', + ), + AppBottomNavigationBarItem( + icon: Icons.calendar_today_outlined, + activeIcon: Icons.calendar_today, + label: 'Jadwal', + ), + AppBottomNavigationBarItem( + icon: Icons.add_box_outlined, + activeIcon: Icons.add_box, + label: 'Penitipan', + ), + AppBottomNavigationBarItem( + icon: Icons.assignment_outlined, + activeIcon: Icons.assignment, + label: 'Laporan', + ), + ], + )), + ); + } + + Widget _buildDrawer(BuildContext context) { + return Drawer( + child: Column( + children: [ + Container( + decoration: BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 16, + bottom: 24, + left: 16, + right: 16), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: CircleAvatar( + radius: 40, + backgroundColor: Colors.white70, + backgroundImage: controller.profilePhotoUrl != null && + controller.profilePhotoUrl!.isNotEmpty + ? NetworkImage(controller.profilePhotoUrl!) + : null, + child: controller.profilePhotoUrl == null || + controller.profilePhotoUrl!.isEmpty + ? const Icon( + Icons.person, + color: Colors.white, + size: 40, + ) + : null, + ), + ), + const SizedBox(height: 16), + const Text( + 'Halo,', + style: TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + Text( + controller.nama, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 22, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Donatur', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on, + color: Colors.white, + size: 14, + ), + const SizedBox(width: 4), + Text( + controller.desa ?? 'Tidak ada desa', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + leading: const Icon(Icons.person_outline), + title: const Text('Profil'), + onTap: () { + Navigator.pop(context); + Get.toNamed('/profile'); + }, + ), + ListTile( + leading: const Icon(Icons.history), + title: const Text('Riwayat Donasi'), + onTap: () { + Navigator.pop(context); + // TODO: Implementasi riwayat donasi + }, + ), + ListTile( + leading: const Icon(Icons.settings_outlined), + title: const Text('Pengaturan'), + onTap: () { + Navigator.pop(context); + // TODO: Implementasi pengaturan + }, + ), + const Divider(), + ListTile( + leading: const Icon(Icons.logout), + title: const Text('Keluar'), + onTap: () { + Navigator.pop(context); + controller.logout(); + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 7de0bd5..e3b15e3 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -36,6 +36,8 @@ import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_peny import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart'; import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart'; import 'package:penyaluran_app/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_view.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; part 'app_routes.dart'; @@ -159,17 +161,17 @@ class AppPages { binding: LaporanPenyaluranBinding(), ), GetPage( - name: _Paths.laporanPenyaluran + '/detail', + name: '${_Paths.laporanPenyaluran}/detail', page: () => const LaporanPenyaluranDetailView(), binding: LaporanPenyaluranBinding(), ), GetPage( - name: _Paths.laporanPenyaluran + '/create', + name: '${_Paths.laporanPenyaluran}/create', page: () => const LaporanPenyaluranCreateView(), binding: LaporanPenyaluranBinding(), ), GetPage( - name: _Paths.laporanPenyaluran + '/edit', + name: '${_Paths.laporanPenyaluran}/edit', page: () => const LaporanPenyaluranEditView(), binding: LaporanPenyaluranBinding(), ), @@ -186,5 +188,50 @@ class AppPages { ), binding: PenyaluranBinding(), ), + GetPage( + name: Routes.donaturDashboard, + page: () => DonaturView(), + binding: DonaturBinding(), + ), + GetPage( + name: _Paths.donaturSkema, + page: () { + final controller = + Get.find(tag: 'donatur_dashboard'); + controller.activeTabIndex.value = 1; + return DonaturView(); + }, + binding: DonaturBinding(), + ), + GetPage( + name: _Paths.donaturJadwal, + page: () { + final controller = + Get.find(tag: 'donatur_dashboard'); + controller.activeTabIndex.value = 2; + return DonaturView(); + }, + binding: DonaturBinding(), + ), + GetPage( + name: _Paths.donaturPenitipan, + page: () { + final controller = + Get.find(tag: 'donatur_dashboard'); + controller.activeTabIndex.value = 3; + return DonaturView(); + }, + binding: DonaturBinding(), + ), + GetPage( + name: _Paths.donaturLaporan, + page: () { + final controller = + Get.find(tag: 'donatur_dashboard'); + controller.activeTabIndex.value = 4; + return DonaturView(); + }, + binding: DonaturBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index cd5b159..c76d8f9 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -11,6 +11,10 @@ abstract class Routes { static const petugasVerifikasiDashboard = _Paths.petugasVerifikasiDashboard; static const petugasDesaDashboard = _Paths.petugasDesaDashboard; static const donaturDashboard = _Paths.donaturDashboard; + static const donaturSkema = _Paths.donaturSkema; + static const donaturJadwal = _Paths.donaturJadwal; + static const donaturPenitipan = _Paths.donaturPenitipan; + static const donaturLaporan = _Paths.donaturLaporan; static const splash = _Paths.splash; static const permintaanPenjadwalan = _Paths.permintaanPenjadwalan; static const daftarPenerima = _Paths.daftarPenerima; @@ -51,6 +55,10 @@ abstract class _Paths { static const petugasVerifikasiDashboard = '/petugas-verifikasi-dashboard'; static const petugasDesaDashboard = '/petugas-desa-dashboard'; static const donaturDashboard = '/donatur-dashboard'; + static const donaturSkema = '/donatur-skema'; + static const donaturJadwal = '/donatur-jadwal'; + static const donaturPenitipan = '/donatur-penitipan'; + static const donaturLaporan = '/donatur-laporan'; static const splash = '/splash'; static const permintaanPenjadwalan = '/permintaan-penjadwalan'; static const daftarPenerima = '/daftar-penerima';