From ca6c28f3d637323c16441d0450d33bbf7dbaab99 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Thu, 10 Apr 2025 14:25:41 +0700 Subject: [PATCH] Perbarui model Pengaduan dengan menambahkan getter isUang untuk memeriksa jenis bantuan. Modifikasi tampilan dan controller di modul donatur dan petugas desa untuk meningkatkan pengalaman pengguna, termasuk penggantian ikon dan penyesuaian format tampilan jumlah bantuan. Hapus kode yang tidak diperlukan untuk menjaga kebersihan kode. --- .../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 +- .../arm64-v8a/configure_fingerprint.bin | 24 +- .../armeabi-v7a/configure_fingerprint.bin | 24 +- .../1o2r2b1v/x86/configure_fingerprint.bin | 24 +- .../1o2r2b1v/x86_64/configure_fingerprint.bin | 24 +- lib/app/data/models/pengaduan_model.dart | 5 + lib/app/data/providers/auth_provider.dart | 93 +- .../auth/controllers/auth_controller.dart | 150 +- .../auth/views/register_donatur_view.dart | 854 +++++----- .../donatur/views/donatur_dashboard_view.dart | 79 +- .../donatur/views/donatur_penitipan_view.dart | 10 +- .../modules/donatur/views/donatur_view.dart | 375 +---- .../components/calendar_view_widget.dart | 195 ++- .../components/greeting_header.dart | 212 ++- .../petugas_desa_dashboard_controller.dart | 9 + .../views/daftar_donatur_view.dart | 2 +- .../petugas_desa/views/dashboard_view.dart | 1499 ++++++++++++----- .../views/detail_donatur_view.dart | 2 +- .../views/detail_pengaduan_view.dart | 129 +- .../views/konfirmasi_penerima_page.dart | 2 +- .../petugas_desa/views/pengaduan_view.dart | 10 +- .../petugas_desa/views/petugas_desa_view.dart | 456 ++--- .../petugas_desa/views/riwayat_stok_view.dart | 22 +- .../petugas_desa/views/stok_bantuan_view.dart | 5 +- .../views/tambah_penyaluran_view.dart | 2 +- .../warga/views/detail_pengaduan_view.dart | 79 +- .../warga/views/warga_dashboard_view.dart | 50 +- .../views/warga_detail_penerimaan_view.dart | 86 +- .../warga/views/warga_penerimaan_view.dart | 11 +- .../warga/views/warga_pengaduan_view.dart | 13 +- lib/app/modules/warga/views/warga_view.dart | 340 +--- lib/app/routes/app_routes.dart | 2 + lib/app/services/supabase_service.dart | 35 + lib/app/widgets/app_drawer.dart | 382 ++++- lib/app/widgets/bantuan_card.dart | 55 +- pubspec.lock | 16 + pubspec.yaml | 1 + 40 files changed, 3103 insertions(+), 2270 deletions(-) diff --git a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin index 693bc59..7fc6f2e 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 4488132..c8ea915 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 113b1ac..7c00614 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 0375bf0..9da809b 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/android/app/.cxx/RelWithDebInfo/1o2r2b1v/arm64-v8a/configure_fingerprint.bin b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/arm64-v8a/configure_fingerprint.bin index ad17a8f..16833bb 100644 --- a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/arm64-v8a/configure_fingerprint.bin +++ b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/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\RelWithDebInfo\1o2r2b1v\arm64-v8a\additional_project_files.txt  2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\additional_project_files.txt  2  2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\android_gradle_build.json  2 2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\android_gradle_build.json  2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\android_gradle_build_mini.json  2 2y +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\android_gradle_build_mini.json  2 2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build.ninja  2 2} +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build.ninja  2 2} { -yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build.ninja.txt  2 +yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build.ninja.txt  2  -~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build_file_index.txt  2 K 2 +~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\build_file_index.txt  2 K 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\compile_commands.json  2  +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\compile_commands.json  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\compile_commands.json.bin  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\compile_commands.json.bin  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\metadata_generation_command.txt  2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\metadata_generation_command.txt  2  2 ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\prefab_config.json  2  ( 2 +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\prefab_config.json  2  ( 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\symbol_folder_index.txt  2  x 2 \ No newline at end of file +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\arm64-v8a\symbol_folder_index.txt  2  x 2 \ No newline at end of file diff --git a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/armeabi-v7a/configure_fingerprint.bin b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/armeabi-v7a/configure_fingerprint.bin index d5afd90..c8f8fc2 100644 --- a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/armeabi-v7a/configure_fingerprint.bin +++ b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/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\RelWithDebInfo\1o2r2b1v\armeabi-v7a\additional_project_files.txt  2  Т2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\additional_project_files.txt  2  Т2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\android_gradle_build.json  2 ڢ2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\android_gradle_build.json  2 ڢ2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\android_gradle_build_mini.json  2 2{ +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\android_gradle_build_mini.json  2 2{ y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build.ninja  2 2 +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build.ninja  2 2 } -{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build.ninja.txt  2 +{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build.ninja.txt  2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build_file_index.txt  2 K 2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\build_file_index.txt  2 K 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\compile_commands.json  2  +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\compile_commands.json  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\compile_commands.json.bin  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\compile_commands.json.bin  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\metadata_generation_command.txt  2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\metadata_generation_command.txt  2  2  -~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\prefab_config.json  2  ( 2 +~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\prefab_config.json  2  ( 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\symbol_folder_index.txt  2  z 2 \ No newline at end of file +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\armeabi-v7a\symbol_folder_index.txt  2  z 2 \ No newline at end of file diff --git a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86/configure_fingerprint.bin b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86/configure_fingerprint.bin index b3e736e..8b63bad 100644 --- a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86/configure_fingerprint.bin +++ b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/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  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\additional_project_files.txt  2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\additional_project_files.txt  2  2  -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\android_gradle_build.json  2 2 +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\android_gradle_build.json  2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\android_gradle_build_mini.json  2 2s +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\android_gradle_build_mini.json  2 2s q -oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build.ninja  2 2w +oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build.ninja  2 2w u -sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build.ninja.txt  2| +sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build.ninja.txt  2| z -xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build_file_index.txt  2 K 2} +xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\build_file_index.txt  2 K 2} { -yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\compile_commands.json  2  +yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\compile_commands.json  2   -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\compile_commands.json.bin  2 +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\compile_commands.json.bin  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\metadata_generation_command.txt  2  2z +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\metadata_generation_command.txt  2  2z x -vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\prefab_config.json  2  ( 2 +vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\prefab_config.json  2  ( 2 } -{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\symbol_folder_index.txt  2  r 2 \ No newline at end of file +{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86\symbol_folder_index.txt  2  r 2 \ No newline at end of file diff --git a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86_64/configure_fingerprint.bin b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86_64/configure_fingerprint.bin index 6fd6255..a6a0fb8 100644 --- a/android/app/.cxx/RelWithDebInfo/1o2r2b1v/x86_64/configure_fingerprint.bin +++ b/android/app/.cxx/RelWithDebInfo/1o2r2b1v/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  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\additional_project_files.txt  2  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\additional_project_files.txt  2  2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\android_gradle_build.json  2 2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\android_gradle_build.json  2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\android_gradle_build_mini.json  2 2v +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\android_gradle_build_mini.json  2 2v t -rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build.ninja  2 2z +rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build.ninja  2 2z x -vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build.ninja.txt  2 +vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build.ninja.txt  2 } -{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build_file_index.txt  2 K 2 +{D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\build_file_index.txt  2 K 2 ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\compile_commands.json  2  +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\compile_commands.json  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\compile_commands.json.bin  2 +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\compile_commands.json.bin  2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\metadata_generation_command.txt  2  2} +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\metadata_generation_command.txt  2  2} { -yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\prefab_config.json  2  ( 2 +yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\prefab_config.json  2  ( 2  -~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\symbol_folder_index.txt  2  u 2 \ No newline at end of file +~D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\RelWithDebInfo\1o2r2b1v\x86_64\symbol_folder_index.txt  2  u 2 \ No newline at end of file diff --git a/lib/app/data/models/pengaduan_model.dart b/lib/app/data/models/pengaduan_model.dart index c042905..1c3501a 100644 --- a/lib/app/data/models/pengaduan_model.dart +++ b/lib/app/data/models/pengaduan_model.dart @@ -111,4 +111,9 @@ class PengaduanModel { if (jumlah == null) return 'Tidak diketahui'; return '$jumlah $satuan'; } + + // Getter untuk memeriksa apakah bantuan berupa uang + bool get isUang { + return stokBantuan?['is_uang'] == true; + } } diff --git a/lib/app/data/providers/auth_provider.dart b/lib/app/data/providers/auth_provider.dart index 0207fa8..235898e 100644 --- a/lib/app/data/providers/auth_provider.dart +++ b/lib/app/data/providers/auth_provider.dart @@ -177,14 +177,13 @@ class AuthProvider { String? jenis, }) async { try { - // Step 1: Daftarkan user dengan role_id 3 (donatur) + print('DEBUG: Memulai proses registrasi donatur dengan email: $email'); + + // Step 1: Daftarkan user tanpa metadata dulu + // Karena mungkin ada masalah dengan kolom role_id custom final response = await _supabaseService.client.auth.signUp( email: email, password: password, - data: { - 'role_id': 3, // Otomatis set sebagai donatur - }, - // Tidak menggunakan email redirect karena kita akan auto-confirm ); final user = response.user; @@ -192,59 +191,63 @@ class AuthProvider { throw Exception('Gagal membuat akun donatur'); } - print('User berhasil terdaftar dengan ID: ${user.id}'); + print('DEBUG: User berhasil terdaftar dengan ID: ${user.id}'); - // Step 2: Buat data donatur di tabel donatur - await _supabaseService.client.from('donatur').insert({ - 'id': user.id, - 'nama_lengkap': namaLengkap, - 'alamat': alamat, - 'no_hp': noHp, - 'email': email, - 'jenis': jenis ?? 'Individu', - 'created_at': DateTime.now().toIso8601String(), - }); - - // Step 3: Pastikan di tabel profiles_backup juga ada data ini (jika digunakan) try { - await _supabaseService.client.from('profiles_backup').insert({ + // Step 2: Buat data donatur di tabel donatur + print('DEBUG: Mencoba menyimpan data donatur ke tabel donatur'); + + final currentTime = DateTime.now().toIso8601String(); + + // Data untuk tabel donatur + final donaturData = { 'id': user.id, - 'updated_at': DateTime.now().toIso8601String(), - 'role_id': 3, - }); + 'nama_lengkap': namaLengkap, + 'alamat': alamat, + 'no_hp': noHp, + 'email': email, + 'jenis': jenis ?? 'Individu', + 'created_at': currentTime, + 'updated_at': currentTime, + }; + + // Insert ke tabel donatur + print('DEBUG: Inserting data to donatur table'); + await _supabaseService.client.from('donatur').insert(donaturData); + print('DEBUG: Berhasil menyimpan data donatur'); + + // Step 3: Coba update role_id user menggunakan service yang lebih aman + print('DEBUG: Mencoba mengupdate role_id user'); + await _supabaseService.updateUserRole(user.id, 3); } catch (e) { - print('Error menyimpan di profiles_backup: $e'); - // Lanjutkan proses meskipun ada error di step ini + print('ERROR: Gagal menyimpan data donatur: $e'); + throw Exception('Gagal menyimpan data donatur. Silakan coba lagi.'); } - print('Berhasil mendaftarkan donatur: $namaLengkap'); - - // Pesan untuk pengembang print( - 'CATATAN: User akan perlu login manual. Jika email konfirmasi masih diperlukan,'); - print( - 'nonaktifkan verifikasi email di dashboard Supabase: Authentication > Email Templates > Disable Email Confirmation'); + 'DEBUG: Berhasil mendaftarkan donatur: $namaLengkap dengan ID: ${user.id}'); } catch (e) { - print('Error pada signUpDonatur: $e'); + print('ERROR: Error pada signUpDonatur: $e'); if (e.toString().contains('User already registered')) { throw Exception( 'Email sudah terdaftar. Silakan gunakan email lain atau login dengan email tersebut.'); + } else if (e.toString().contains('Database error saving new user')) { + throw Exception( + 'Terjadi kesalahan saat menyimpan data pengguna. Silakan coba lagi atau hubungi administrator.'); + } else if (e.toString().contains('Error sending confirmation mail')) { + print('===== PERHATIAN PENGEMBANG ====='); + print( + 'Error ini terjadi karena Supabase gagal mengirim email konfirmasi.'); + print( + 'Untuk mengatasi ini, nonaktifkan konfirmasi email di dashboard Supabase:'); + print( + '1. Buka project Supabase > Authentication > Email Templates > Confirmation'); + print('2. Matikan toggle "Enable email confirmations"'); + print('=============================='); + throw Exception( + 'Gagal mengirim email konfirmasi. Mohon hubungi administrator.'); } else { - // Untuk error konfirmasi email, berikan instruksi yang jelas - if (e.toString().contains('Error sending confirmation mail')) { - print('===== PERHATIAN PENGEMBANG ====='); - print( - 'Error ini terjadi karena Supabase gagal mengirim email konfirmasi.'); - print( - 'Untuk mengatasi ini, nonaktifkan konfirmasi email di dashboard Supabase:'); - print( - '1. Buka project Supabase > Authentication > Email Templates > Confirmation'); - print('2. Matikan toggle "Enable email confirmations"'); - print('=============================='); - throw Exception( - 'Gagal mengirim email konfirmasi. Mohon hubungi administrator.'); - } rethrow; } } diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart index f6a82ef..1830fe9 100644 --- a/lib/app/modules/auth/controllers/auth_controller.dart +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -537,62 +537,114 @@ class AuthController extends GetxController { } isLoading.value = true; - try { - // Proses registrasi donatur dengan role_id 3 - await _authProvider.signUpDonatur( - email: emailController.text, - password: passwordController.text, - namaLengkap: namaController.text, - alamat: alamatController.text, - noHp: noHpController.text, - jenis: jenisController.text.isEmpty ? 'Individu' : jenisController.text, - ); - Get.snackbar( - 'Sukses', - 'Registrasi donatur berhasil! Silakan login dengan akun Anda.', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green, - colorText: Colors.white, - duration: const Duration(seconds: 5), - ); + // Kita akan mencoba maksimal 2 kali jika terjadi error + int maxAttempts = 2; + int attempt = 0; + bool success = false; + Exception? lastError; - // Bersihkan form - clearDonaturRegistrationForm(); + while (attempt < maxAttempts && !success) { + attempt++; + print( + 'DEBUG: Mencoba pendaftaran donatur (percobaan ke-$attempt dari $maxAttempts)'); - // Arahkan ke halaman login - Get.offAllNamed(Routes.login); - } catch (e) { - print('Error registrasi donatur: $e'); + try { + print('DEBUG: Validasi form berhasil, melanjutkan proses registrasi'); + print('DEBUG: Email: ${emailController.text}'); + print('DEBUG: Nama: ${namaController.text}'); + print('DEBUG: Alamat: ${alamatController.text}'); + print('DEBUG: No HP: ${noHpController.text}'); + print( + 'DEBUG: Jenis: ${jenisController.text.isEmpty ? 'Individu' : jenisController.text}'); - String errorMessage = 'Gagal melakukan registrasi'; + // Proses registrasi donatur + await _authProvider.signUpDonatur( + email: emailController.text, + password: passwordController.text, + namaLengkap: namaController.text, + alamat: alamatController.text, + noHp: noHpController.text, + jenis: + jenisController.text.isEmpty ? 'Individu' : jenisController.text, + ); - // Tangani error sesuai jenisnya - if (e.toString().contains('email konfirmasi')) { - errorMessage = - 'Gagal mengirim email konfirmasi. Mohon periksa alamat email Anda dan coba lagi nanti.'; - } else if (e.toString().contains('Email sudah terdaftar')) { - errorMessage = - 'Email sudah terdaftar. Silakan gunakan email lain atau login dengan email tersebut.'; - } else if (e.toString().contains('weak-password')) { - errorMessage = - 'Password terlalu lemah. Gunakan kombinasi huruf, angka, dan simbol.'; - } else if (e.toString().contains('invalid-email')) { - errorMessage = 'Format email tidak valid.'; - } else { - errorMessage = 'Gagal melakukan registrasi: ${e.toString()}'; + print('DEBUG: Registrasi donatur berhasil'); + success = true; + + Get.snackbar( + 'Sukses', + 'Registrasi donatur berhasil! Silakan login dengan akun Anda.', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 5), + ); + + // Bersihkan form + clearDonaturRegistrationForm(); + + // Arahkan ke halaman login + Get.offAllNamed(Routes.login); + } catch (e) { + lastError = e is Exception ? e : Exception(e.toString()); + print('ERROR: Error registrasi donatur (percobaan ke-$attempt): $e'); + print('ERROR Detail: ${e.runtimeType} - ${e.toString()}'); + + // Cek apakah bisa dicoba lagi atau tidak + bool canRetry = false; + if (e.toString().contains('Database error saving new user')) { + canRetry = true; // Error database bisa dicoba lagi + print('DEBUG: Error database, akan mencoba lagi'); + // Delay sedikit sebelum mencoba lagi + await Future.delayed(const Duration(seconds: 1)); + } else if (e.toString().contains('User already registered')) { + // Jangan coba lagi jika email sudah terdaftar + canRetry = false; + } + + if (!canRetry || attempt >= maxAttempts) { + // Jika tidak bisa retry atau sudah maksimal, tampilkan error + String errorMessage = 'Gagal melakukan registrasi'; + + // Tangani error sesuai jenisnya + if (e.toString().contains('email konfirmasi')) { + errorMessage = + 'Gagal mengirim email konfirmasi. Mohon periksa alamat email Anda dan coba lagi nanti.'; + } else if (e.toString().contains('Email sudah terdaftar')) { + errorMessage = + 'Email sudah terdaftar. Silakan gunakan email lain atau login dengan email tersebut.'; + } else if (e.toString().contains('weak-password')) { + errorMessage = + 'Password terlalu lemah. Gunakan kombinasi huruf, angka, dan simbol.'; + } else if (e.toString().contains('invalid-email')) { + errorMessage = 'Format email tidak valid.'; + } else if (e.toString().contains('Database error')) { + errorMessage = + 'Terjadi kesalahan pada database. Silakan coba lagi nanti atau hubungi administrator.'; + } else { + errorMessage = 'Gagal melakukan registrasi: ${e.toString()}'; + } + + Get.snackbar( + 'Error', + errorMessage, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 7), + ); + break; // Keluar dari loop + } } + } - Get.snackbar( - 'Error', - errorMessage, - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red, - colorText: Colors.white, - duration: const Duration(seconds: 5), - ); - } finally { - isLoading.value = false; + isLoading.value = false; + + // Jika gagal setelah mencoba beberapa kali + if (!success && lastError != null) { + print('ERROR: Pendaftaran gagal setelah $attempt percobaan'); + // Error sudah ditampilkan di dalam loop } } diff --git a/lib/app/modules/auth/views/register_donatur_view.dart b/lib/app/modules/auth/views/register_donatur_view.dart index f3aee8e..21b9c2a 100644 --- a/lib/app/modules/auth/views/register_donatur_view.dart +++ b/lib/app/modules/auth/views/register_donatur_view.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; class RegisterDonaturView extends GetView { const RegisterDonaturView({super.key}); @@ -13,7 +14,7 @@ class RegisterDonaturView extends GetView { appBar: AppBar( title: const Text('Daftar Donatur'), centerTitle: true, - backgroundColor: Colors.blue, + backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, elevation: 0, shape: const RoundedRectangleBorder( @@ -22,436 +23,469 @@ class RegisterDonaturView extends GetView { ), ), ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: SingleChildScrollView( - child: Form( - key: controller.registerDonaturFormKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 10), - // Header dengan icon dan judul - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(15), - ), - child: Column( - children: [ - Image.asset( - 'assets/images/logo-disalurkita.png', - width: 120, - height: 120, - ), - const Text( - 'Daftar Sebagai Donatur', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.blue, + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFE3F2FD), Colors.white], + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: SingleChildScrollView( + child: Form( + key: controller.registerDonaturFormKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 10), + // Header dengan icon dan judul + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 2), ), - ), - const SizedBox(height: 4), - const Text( - 'Bergabunglah dengan kami untuk membantu mereka yang membutuhkan', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.blueGrey, + ], + ), + child: Column( + children: [ + Image.asset( + 'assets/images/logo-disalurkita.png', + width: 120, + height: 120, ), - ), - ], - ), - ), - const SizedBox(height: 20), - // Step indicator - const Row( - children: [ - Icon(Icons.person_add, color: Colors.blue), - SizedBox(width: 10), - Text( - 'Informasi Akun', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - ], - ), - const SizedBox(height: 15), - const Divider(), - const SizedBox(height: 10), - - // Nama Lengkap - TextFormField( - controller: controller.namaController, - keyboardType: TextInputType.name, - decoration: InputDecoration( - labelText: 'Nama Lengkap', - hintText: 'Masukkan nama lengkap Anda', - prefixIcon: const Icon(Icons.person, color: Colors.blue), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - ), - validator: controller.validateDonaturNama, - ), - const SizedBox(height: 15), - - // Email - TextFormField( - controller: controller.emailController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email', - hintText: 'contoh@email.com', - prefixIcon: const Icon(Icons.email, color: Colors.blue), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - ), - validator: controller.validateEmail, - ), - const SizedBox(height: 15), - - // Password - Obx(() => TextFormField( - controller: controller.passwordController, - obscureText: controller.isPasswordHidden.value, - decoration: InputDecoration( - labelText: 'Password', - hintText: 'Minimal 8 karakter', - prefixIcon: - const Icon(Icons.lock, color: Colors.blue), - suffixIcon: IconButton( - icon: Icon( - controller.isPasswordHidden.value - ? Icons.visibility_off - : Icons.visibility, - color: Colors.blue, + Text( + 'Daftar Sebagai Donatur', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, ), - onPressed: () => - controller.togglePasswordVisibility(), ), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - ), - validator: controller.validatePassword, - )), - const SizedBox(height: 15), - - // Confirm Password - Obx(() => TextFormField( - controller: controller.confirmPasswordController, - obscureText: controller.isConfirmPasswordHidden.value, - decoration: InputDecoration( - labelText: 'Konfirmasi Password', - hintText: 'Masukkan password yang sama', - prefixIcon: const Icon(Icons.lock_outline, - color: Colors.blue), - suffixIcon: IconButton( - icon: Icon( - controller.isConfirmPasswordHidden.value - ? Icons.visibility_off - : Icons.visibility, - color: Colors.blue, + const SizedBox(height: 4), + Text( + 'Bergabunglah dengan kami untuk membantu mereka yang membutuhkan', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: const Color(0xFF546E7A), ), - onPressed: () => - controller.toggleConfirmPasswordVisibility(), ), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - ), - validator: controller.validateConfirmPassword, - )), - const SizedBox(height: 15), - - // Section heading - const Row( - children: [ - Icon(Icons.person_pin_circle, color: Colors.blue), - SizedBox(width: 10), - Text( - 'Informasi Profil', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - ], - ), - const SizedBox(height: 15), - const Divider(), - const SizedBox(height: 10), - - // No HP - TextFormField( - controller: controller.noHpController, - keyboardType: TextInputType.phone, - decoration: InputDecoration( - labelText: 'Nomor HP', - hintText: 'Masukkan nomor HP aktif', - prefixIcon: const Icon(Icons.phone, color: Colors.blue), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), + ], ), ), - validator: controller.validateDonaturNoHp, - ), - const SizedBox(height: 15), - - // Alamat - TextFormField( - controller: controller.alamatController, - keyboardType: TextInputType.streetAddress, - maxLines: 2, - decoration: InputDecoration( - labelText: 'Alamat Lengkap', - hintText: 'Masukkan alamat lengkap Anda', - prefixIcon: const Icon(Icons.home, color: Colors.blue), - filled: true, - fillColor: Colors.grey.shade100, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.blue, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Colors.red), - ), - ), - validator: controller.validateDonaturAlamat, - ), - const SizedBox(height: 15), - - // Jenis Donatur (Dropdown) - Container( - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.grey.shade300), - ), - child: DropdownButtonFormField( - value: controller.jenisController.text.isEmpty - ? 'Individu' - : controller.jenisController.text, - decoration: InputDecoration( - labelText: 'Jenis Donatur', - prefixIcon: - const Icon(Icons.category, color: Colors.blue), - border: InputBorder.none, - contentPadding: - const EdgeInsets.symmetric(horizontal: 10), - ), - items: const [ - DropdownMenuItem( - value: 'Individu', child: Text('Individu')), - DropdownMenuItem( - value: 'Organisasi', child: Text('Organisasi')), - DropdownMenuItem( - value: 'Perusahaan', child: Text('Perusahaan')), - DropdownMenuItem( - value: 'Lainnya', child: Text('Lainnya')), - ], - onChanged: (value) { - controller.jenisController.text = value ?? 'Individu'; - }, - ), - ), - - const SizedBox(height: 25), - - // Catatan Informasi - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.blue.shade200), - ), - child: Row( + const SizedBox(height: 20), + // Step indicator + Row( children: [ - const Icon(Icons.info_outline, color: Colors.blue), + Icon(Icons.person_add, color: AppTheme.primaryColor), const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text( - 'Informasi', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue, - ), + Text( + 'Informasi Akun', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + const SizedBox(height: 15), + const Divider(), + const SizedBox(height: 10), + + // Nama Lengkap + TextFormField( + controller: controller.namaController, + keyboardType: TextInputType.name, + decoration: InputDecoration( + labelText: 'Nama Lengkap', + hintText: 'Masukkan nama lengkap Anda', + prefixIcon: + Icon(Icons.person, color: AppTheme.primaryColor), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validateDonaturNama, + ), + const SizedBox(height: 15), + + // Email + TextFormField( + controller: controller.emailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email', + hintText: 'contoh@email.com', + prefixIcon: + Icon(Icons.email, color: AppTheme.primaryColor), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validateEmail, + ), + const SizedBox(height: 15), + + // Password + Obx(() => TextFormField( + controller: controller.passwordController, + obscureText: controller.isPasswordHidden.value, + decoration: InputDecoration( + labelText: 'Password', + hintText: 'Minimal 8 karakter', + prefixIcon: + Icon(Icons.lock, color: AppTheme.primaryColor), + suffixIcon: IconButton( + icon: Icon( + controller.isPasswordHidden.value + ? Icons.visibility_off + : Icons.visibility, + color: AppTheme.primaryColor, ), - SizedBox(height: 5), - Text( - 'Data Anda akan terverifikasi dan terlindungi. Kami menjaga privasi dan keamanan data Anda.', - style: TextStyle( - fontSize: 14, - color: Colors.blueGrey, + onPressed: () => + controller.togglePasswordVisibility(), + ), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validatePassword, + )), + const SizedBox(height: 15), + + // Confirm Password + Obx(() => TextFormField( + controller: controller.confirmPasswordController, + obscureText: controller.isConfirmPasswordHidden.value, + decoration: InputDecoration( + labelText: 'Konfirmasi Password', + hintText: 'Masukkan password yang sama', + prefixIcon: Icon(Icons.lock_outline, + color: AppTheme.primaryColor), + suffixIcon: IconButton( + icon: Icon( + controller.isConfirmPasswordHidden.value + ? Icons.visibility_off + : Icons.visibility, + color: AppTheme.primaryColor, + ), + onPressed: () => + controller.toggleConfirmPasswordVisibility(), + ), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validateConfirmPassword, + )), + const SizedBox(height: 15), + + // Section heading + Row( + children: [ + Icon(Icons.person_pin_circle, + color: AppTheme.primaryColor), + const SizedBox(width: 10), + Text( + 'Informasi Profil', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + const SizedBox(height: 15), + const Divider(), + const SizedBox(height: 10), + + // No HP + TextFormField( + controller: controller.noHpController, + keyboardType: TextInputType.phone, + decoration: InputDecoration( + labelText: 'Nomor HP', + hintText: 'Masukkan nomor HP aktif', + prefixIcon: + Icon(Icons.phone, color: AppTheme.primaryColor), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validateDonaturNoHp, + ), + const SizedBox(height: 15), + + // Alamat + TextFormField( + controller: controller.alamatController, + keyboardType: TextInputType.streetAddress, + maxLines: 2, + decoration: InputDecoration( + labelText: 'Alamat Lengkap', + hintText: 'Masukkan alamat lengkap Anda', + prefixIcon: + Icon(Icons.home, color: AppTheme.primaryColor), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: AppTheme.primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.red), + ), + ), + validator: controller.validateDonaturAlamat, + ), + const SizedBox(height: 15), + + // Jenis Donatur (Dropdown) + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: DropdownButtonFormField( + value: controller.jenisController.text.isEmpty + ? 'Individu' + : controller.jenisController.text, + decoration: InputDecoration( + labelText: 'Jenis Donatur', + labelStyle: TextStyle(color: AppTheme.primaryColor), + prefixIcon: Icon(Icons.category, + color: AppTheme.primaryColor), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 10), + ), + items: const [ + DropdownMenuItem( + value: 'Individu', child: Text('Individu')), + DropdownMenuItem( + value: 'Organisasi', child: Text('Organisasi')), + DropdownMenuItem( + value: 'Perusahaan', child: Text('Perusahaan')), + DropdownMenuItem( + value: 'Lainnya', child: Text('Lainnya')), + ], + onChanged: (value) { + controller.jenisController.text = value ?? 'Individu'; + }, + dropdownColor: Colors.white, + icon: Icon(Icons.arrow_drop_down, + color: AppTheme.primaryColor), + ), + ), + + const SizedBox(height: 25), + + // Catatan Informasi + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: const Color(0xFFF1F8E9), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: const Color(0xFFAED581)), + ), + child: Row( + children: [ + const Icon(Icons.info_outline, + color: Color(0xFF558B2F)), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Informasi', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF558B2F), + ), ), + SizedBox(height: 5), + Text( + 'Data Anda akan terverifikasi dan terlindungi. Kami menjaga privasi dan keamanan data Anda.', + style: TextStyle( + fontSize: 14, + color: Color(0xFF33691E), + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 25), + + // Register Button + Obx(() => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 2), ), ], ), - ), - ], - ), - ), - - const SizedBox(height: 25), - - // Register Button - Obx(() => Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.3), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 2), + child: ElevatedButton( + onPressed: controller.isLoading.value + ? null + : controller.registerDonatur, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 15), + backgroundColor: AppTheme.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, ), - ], - ), - child: ElevatedButton( - onPressed: controller.isLoading.value - ? null - : controller.registerDonatur, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 15), - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 0, - ), - child: controller.isLoading.value - ? const SpinKitThreeBounce( - color: Colors.white, - size: 24, - ) - : const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.how_to_reg, color: Colors.white), - SizedBox(width: 10), - Text( - 'DAFTAR SEKARANG', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, + child: controller.isLoading.value + ? const SpinKitThreeBounce( + color: Colors.white, + size: 24, + ) + : const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.how_to_reg, + color: Colors.white), + SizedBox(width: 10), + Text( + 'DAFTAR SEKARANG', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1, + ), ), - ), - ], - ), - ), - )), - const SizedBox(height: 20), + ], + ), + ), + )), + const SizedBox(height: 20), - // Login Link - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.grey.shade200), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Sudah punya akun?', - style: TextStyle(color: Colors.grey), - ), - TextButton( - onPressed: () => Get.offAllNamed(Routes.login), - child: const Text( - 'Masuk', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue, + // Login Link + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Sudah punya akun?', + style: TextStyle(color: Color(0xFF546E7A)), + ), + TextButton( + onPressed: () => Get.offAllNamed(Routes.login), + style: TextButton.styleFrom( + foregroundColor: AppTheme.primaryColor, + ), + child: const Text( + 'Masuk', + style: TextStyle( + fontWeight: FontWeight.bold, + ), ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 20), - ], + const SizedBox(height: 20), + ], + ), ), ), ), diff --git a/lib/app/modules/donatur/views/donatur_dashboard_view.dart b/lib/app/modules/donatur/views/donatur_dashboard_view.dart index 87997f3..84bbc9e 100644 --- a/lib/app/modules/donatur/views/donatur_dashboard_view.dart +++ b/lib/app/modules/donatur/views/donatur_dashboard_view.dart @@ -36,54 +36,6 @@ class DonaturDashboardView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header DisalurKita dengan logo dan slogan - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Image.asset( - 'assets/images/logo-disalurkita.png', - width: 50, - height: 50, - ), - const SizedBox(width: 15), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'DisalurKita', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF1565C0), - ), - ), - SizedBox(height: 5), - Text( - 'Salurkan dengan Pasti, Pantau dengan Bukti', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ), - ), _buildWelcomeSection(), const SizedBox(height: 24), _buildStatisticSection(), @@ -242,29 +194,14 @@ class DonaturDashboardView extends GetView { ), ), 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; - }, - ), - ), - ], + _buildActionButton( + icon: Icons.add_box_rounded, + label: 'Titip Bantuan', + color: Colors.green.shade700, + onTap: () { + // Navigasi ke form penitipan bantuan + controller.activeTabIndex.value = 3; + }, ), ], ), diff --git a/lib/app/modules/donatur/views/donatur_penitipan_view.dart b/lib/app/modules/donatur/views/donatur_penitipan_view.dart index a07031a..d6f79b2 100644 --- a/lib/app/modules/donatur/views/donatur_penitipan_view.dart +++ b/lib/app/modules/donatur/views/donatur_penitipan_view.dart @@ -661,7 +661,8 @@ class _FormPenitipanBantuanState extends State { children: [ Expanded( child: _buildInfoItem( - icon: isUang ? Icons.attach_money : Icons.category, + icon: + isUang ? Icons.payment_rounded : Icons.category, label: 'Jenis Bantuan', value: stokNama, iconColor: isUang @@ -944,7 +945,8 @@ class _FormPenitipanBantuanState extends State { children: [ Expanded( child: _buildInfoItem( - icon: isUang ? Icons.attach_money : Icons.category, + icon: + isUang ? Icons.payment_rounded : Icons.category, label: 'Jenis Bantuan', value: stokNama, iconColor: isUang @@ -1042,7 +1044,7 @@ class _FormPenitipanBantuanState extends State { children: [ Icon( // Tampilkan ikon uang jika stok bantuan berbentuk uang - isUang ? Icons.attach_money : Icons.numbers, + isUang ? Icons.payment_rounded : Icons.numbers, color: Colors.grey.shade700), const SizedBox(width: 8), Text( @@ -1660,7 +1662,7 @@ class _FormPenitipanBantuanState extends State { const Divider(), _buildDetailRow( icon: isUangBantuan - ? Icons.attach_money + ? Icons.payment_rounded : Icons.shopping_bag, label: 'Jumlah', value: jumlahBantuan + (isUangBantuan ? '' : ' item'), diff --git a/lib/app/modules/donatur/views/donatur_view.dart b/lib/app/modules/donatur/views/donatur_view.dart index c3e6fd1..c951c86 100644 --- a/lib/app/modules/donatur/views/donatur_view.dart +++ b/lib/app/modules/donatur/views/donatur_view.dart @@ -8,6 +8,7 @@ import 'package:penyaluran_app/app/modules/donatur/views/donatur_laporan_view.da import 'package:penyaluran_app/app/modules/donatur/views/donatur_penitipan_view.dart'; import 'package:penyaluran_app/app/modules/donatur/views/donatur_riwayat_penitipan_view.dart'; import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart'; +import 'package:penyaluran_app/app/widgets/app_drawer.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class DonaturView extends GetView { @@ -166,301 +167,93 @@ class DonaturView extends GetView { } 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) - ? Text( - controller.nama.isNotEmpty - ? controller.nama - .toString() - .substring(0, 1) - .toUpperCase() - : '?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue.shade700, - fontSize: 24, - ), - ) - : 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, - ), - ), - ], - ), - ), - ], - ), - ], - ), + return Obx(() { + Map> menuCategories = { + 'Menu Utama': [ + DrawerMenuItem( + icon: Icons.dashboard_outlined, + activeIcon: Icons.dashboard, + title: 'Dashboard', + isSelected: controller.activeTabIndex.value == 0, + onTap: () { + controller.activeTabIndex.value = 0; + }, ), - Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: [ - _buildMenuCategory('Menu Utama'), - Obx(() => _buildMenuItem( - icon: Icons.dashboard_outlined, - activeIcon: Icons.dashboard, - title: 'Dashboard', - isSelected: controller.activeTabIndex.value == 0, - onTap: () { - Navigator.pop(context); - controller.activeTabIndex.value = 0; - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.description_outlined, - activeIcon: Icons.description, - title: 'Skema Bantuan', - isSelected: controller.activeTabIndex.value == 1, - onTap: () { - Navigator.pop(context); - controller.activeTabIndex.value = 1; - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.calendar_today_outlined, - activeIcon: Icons.calendar_today, - title: 'Jadwal Penyaluran', - isSelected: controller.activeTabIndex.value == 2, - onTap: () { - Navigator.pop(context); - controller.activeTabIndex.value = 2; - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.add_box_outlined, - activeIcon: Icons.add_box, - title: 'Penitipan Bantuan', - isSelected: controller.activeTabIndex.value == 3, - onTap: () { - Navigator.pop(context); - controller.activeTabIndex.value = 3; - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.assignment_outlined, - activeIcon: Icons.assignment, - title: 'Laporan Penyaluran', - isSelected: controller.activeTabIndex.value == 4, - onTap: () { - Navigator.pop(context); - controller.activeTabIndex.value = 4; - }, - )), - _buildMenuCategory('Pengaturan'), - _buildMenuItem( - icon: Icons.person_outline, - activeIcon: Icons.person, - title: 'Profil', - onTap: () { - Navigator.pop(context); - Get.toNamed('/profile'); - }, - ), - _buildMenuItem( - icon: Icons.info_outline, - activeIcon: Icons.info, - title: 'Tentang Kami', - onTap: () { - Navigator.pop(context); - Get.toNamed('/about'); - }, - ), - _buildMenuItem( - icon: Icons.logout, - title: 'Keluar', - onTap: () { - Navigator.pop(context); - controller.logout(); - }, - isLogout: true, - ), - ], - ), + DrawerMenuItem( + icon: Icons.description_outlined, + activeIcon: Icons.description, + title: 'Skema Bantuan', + isSelected: controller.activeTabIndex.value == 1, + onTap: () { + controller.activeTabIndex.value = 1; + }, ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - '© ${DateTime.now().year} DisalurKita', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), + DrawerMenuItem( + icon: Icons.calendar_today_outlined, + activeIcon: Icons.calendar_today, + title: 'Jadwal Penyaluran', + isSelected: controller.activeTabIndex.value == 2, + onTap: () { + controller.activeTabIndex.value = 2; + }, + ), + DrawerMenuItem( + icon: Icons.add_box_outlined, + activeIcon: Icons.add_box, + title: 'Penitipan Bantuan', + isSelected: controller.activeTabIndex.value == 3, + onTap: () { + controller.activeTabIndex.value = 3; + }, + ), + DrawerMenuItem( + icon: Icons.assignment_outlined, + activeIcon: Icons.assignment, + title: 'Laporan Penyaluran', + isSelected: controller.activeTabIndex.value == 4, + onTap: () { + controller.activeTabIndex.value = 4; + }, ), ], - ), - ); - } - - Widget _buildMenuCategory(String title) { - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), - child: Text( - title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ); - } - - Widget _buildMenuItem({ - required IconData icon, - IconData? activeIcon, - required String title, - bool isSelected = false, - String? badge, - required Function() onTap, - bool isLogout = false, - }) { - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: BoxDecoration( - color: isSelected - ? AppTheme.primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: ListTile( - leading: Icon( - isSelected ? (activeIcon ?? icon) : icon, - color: isSelected - ? AppTheme.primaryColor - : (isLogout ? Colors.red : null), - ), - title: Text( - title, - style: TextStyle( - color: isSelected - ? AppTheme.primaryColor - : (isLogout ? Colors.red : null), - fontWeight: isSelected ? FontWeight.bold : null, + 'Pengaturan': [ + DrawerMenuItem( + icon: Icons.person_outline, + activeIcon: Icons.person, + title: 'Profil', + onTap: () { + Get.toNamed('/profile'); + }, ), - ), - trailing: badge != null - ? Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 20, - minHeight: 20, - ), - child: Text( - badge, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ) - : null, - onTap: onTap, - ), - ); + DrawerMenuItem( + icon: Icons.info_outline, + activeIcon: Icons.info, + title: 'Tentang Kami', + onTap: () { + Get.toNamed('/about'); + }, + ), + DrawerMenuItem( + icon: Icons.logout, + title: 'Keluar', + isLogout: true, + onTap: () { + controller.logout(); + }, + ), + ], + }; + + return AppDrawer( + nama: controller.nama, + role: 'Donatur', + desa: controller.desa, + avatar: controller.profilePhotoUrl, + menuItems: const [], // Tidak digunakan karena menggunakan menuCategories + menuCategories: menuCategories, + onLogout: controller.logout, + footerText: '© ${DateTime.now().year} DisalurKita', + ); + }); } } diff --git a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart index 95e2e00..2fcf189 100644 --- a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart +++ b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart @@ -10,6 +10,12 @@ class CalendarViewWidget extends StatelessWidget { final JadwalPenyaluranController controller; final CalendarController _calendarController = CalendarController(); + // Tambahkan variabel untuk status filter + final RxBool _showAllSchedules = true.obs; + + // Tambahkan variabel untuk mode tampilan kalender + final Rx _calendarView = CalendarView.month.obs; + CalendarViewWidget({ super.key, required this.controller, @@ -47,20 +53,135 @@ class CalendarViewWidget extends StatelessWidget { topRight: Radius.circular(16), ), ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - Icons.calendar_month_rounded, - color: Colors.white, - size: 28, + // Title pada baris pertama + Row( + children: [ + const Icon( + Icons.calendar_month_rounded, + color: Colors.white, + size: 24, + ), + const SizedBox(width: 12), + Text( + 'Kalender Penyaluran', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], ), - const SizedBox(width: 12), - Text( - 'Kalender Penyaluran', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), + + const SizedBox(height: 12), + + // Tombol filter pada baris kedua + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Tombol mode tampilan + Obx(() => Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Tooltip( + message: _calendarView.value == CalendarView.month + ? 'Beralih ke tampilan agenda' + : 'Beralih ke tampilan kalender', + child: InkWell( + onTap: () { + _calendarView.value = + _calendarView.value == CalendarView.month + ? CalendarView.schedule + : CalendarView.month; + + // Update calendar controller dengan tampilan baru + _calendarController.view = _calendarView.value; + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _calendarView.value == CalendarView.month + ? Icons.view_agenda + : Icons.calendar_month, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 4), + Text( + _calendarView.value == CalendarView.month + ? 'Agenda' + : 'Bulan', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + )), + + const SizedBox(width: 8), + + // Tombol filter + Obx(() => Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Tooltip( + message: _showAllSchedules.value + ? 'Tampilkan hanya jadwal bulan ini' + : 'Tampilkan semua jadwal', + child: InkWell( + onTap: () { + _showAllSchedules.value = + !_showAllSchedules.value; + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _showAllSchedules.value + ? Icons.filter_list + : Icons.filter_alt, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 4), + Text( + _showAllSchedules.value + ? 'Semua' + : 'Bulan Ini', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + )), + ], ), ], ), @@ -71,7 +192,7 @@ class CalendarViewWidget extends StatelessWidget { height: MediaQuery.of(context).size.height * 0.65, child: Obx(() { return SfCalendar( - view: CalendarView.month, + view: _calendarView.value, controller: _calendarController, initialSelectedDate: DateTime.now(), initialDisplayDate: DateTime.now(), @@ -119,6 +240,42 @@ class CalendarViewWidget extends StatelessWidget { ), ), ), + scheduleViewSettings: ScheduleViewSettings( + appointmentItemHeight: 70, + monthHeaderSettings: MonthHeaderSettings( + height: 50, + backgroundColor: AppTheme.primaryColor, + monthTextStyle: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + weekHeaderSettings: WeekHeaderSettings( + height: 40, + textAlign: TextAlign.center, + backgroundColor: Colors.grey.shade100, + weekTextStyle: TextStyle( + color: AppTheme.primaryColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + dayHeaderSettings: DayHeaderSettings( + dayFormat: 'EEEE', + width: 70, + dayTextStyle: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: AppTheme.primaryColor, + ), + dateTextStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ), cellBorderColor: Colors.grey.withOpacity(0.2), todayHighlightColor: AppTheme.primaryColor, selectionDecoration: BoxDecoration( @@ -291,6 +448,7 @@ class CalendarViewWidget extends StatelessWidget { ...controller.jadwalTerlaksana, ]; + // Tambahkan filter berdasarkan _showAllSchedules DateTime now = DateTime.now(); DateTime firstDayOfMonth = DateTime(now.year, now.month, 1); DateTime lastDayOfMonth = DateTime(now.year, now.month + 1, 0); @@ -300,9 +458,14 @@ class CalendarViewWidget extends StatelessWidget { DateTime jadwalDate = FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!); - if (jadwalDate - .isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) && - jadwalDate.isBefore(lastDayOfMonth.add(const Duration(days: 1)))) { + // Filter berdasarkan bulan saat ini jika _showAllSchedules.value = false + bool shouldShow = _showAllSchedules.value || + (jadwalDate.isAfter( + firstDayOfMonth.subtract(const Duration(days: 1))) && + jadwalDate + .isBefore(lastDayOfMonth.add(const Duration(days: 1)))); + + if (shouldShow) { Color appointmentColor; // Periksa status jadwal menggunakan switch-case untuk konsistensi diff --git a/lib/app/modules/petugas_desa/components/greeting_header.dart b/lib/app/modules/petugas_desa/components/greeting_header.dart index c44ebba..249ef5a 100644 --- a/lib/app/modules/petugas_desa/components/greeting_header.dart +++ b/lib/app/modules/petugas_desa/components/greeting_header.dart @@ -1,55 +1,223 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; class GreetingHeader extends StatelessWidget { final String name; final String role; final String? desa; + final String? nip; + final String? profileImageUrl; const GreetingHeader({ super.key, required this.name, required this.role, this.desa, + this.nip, + this.profileImageUrl, }); + String _getGreeting() { + final hour = DateTime.now().hour; + if (hour < 12) { + return 'Selamat Pagi'; + } else if (hour < 15) { + return 'Selamat Siang'; + } else if (hour < 19) { + return 'Selamat Sore'; + } else { + return 'Selamat Malam'; + } + } + + String _getCurrentDate() { + final now = DateTime.now(); + return DateFormat('EEEE, d MMMM yyyy', 'id_ID').format(now); + } + @override Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Colors.blue.shade800, + Colors.blue.shade600, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.grey.withAlpha(26), // 0.1 * 255 ≈ 26 + color: Colors.blue.withOpacity(0.3), spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 1), + blurRadius: 15, + offset: const Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Selamat Datang, $name!', - style: textTheme.headlineSmall?.copyWith( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _getGreeting(), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + name, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + image: profileImageUrl != null && profileImageUrl!.isNotEmpty + ? DecorationImage( + image: NetworkImage(profileImageUrl!), + fit: BoxFit.cover, + ) + : null, + ), + child: profileImageUrl == null || profileImageUrl!.isEmpty + ? const Icon( + Icons.person, + size: 30, + color: Colors.white, + ) + : null, + ), + ], ), - const SizedBox(height: 5), - Text( - desa != null && desa!.isNotEmpty - ? 'Kamu Login Sebagai $role $desa.' - : 'Kamu Login Sebagai $role.', - style: textTheme.bodyMedium?.copyWith( - fontSize: 14, - color: Colors.grey[600], - ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on, + size: 14, + color: Colors.white70, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + desa != null && desa!.isNotEmpty + ? '$role - Desa $desa' + : role, + style: const TextStyle( + fontSize: 12, + color: Colors.white70, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (nip != null && nip!.isNotEmpty) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.badge, + size: 14, + color: Colors.white70, + ), + const SizedBox(width: 4), + Text( + 'NIP: $nip', + style: const TextStyle( + fontSize: 12, + color: Colors.white70, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.calendar_today, + size: 14, + color: Colors.white70, + ), + const SizedBox(width: 4), + Text( + _getCurrentDate(), + style: const TextStyle( + fontSize: 12, + color: Colors.white70, + ), + ), + ], + ), + ), + ], ), ], ), diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart index fae570e..bdd792f 100644 --- a/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart @@ -57,6 +57,15 @@ class PetugasDesaDashboardController extends GetxController { userProfile['desa']?['nama'] ?? (userProfile['desa_id'] != null ? 'Desa' : 'Desa'); + // Getter untuk NIP dari profil pengguna + String? get nip => + userProfile['role_data']?['nip'] ?? _authController.roleData?.nip; + + // Getter untuk foto profil dari profil pengguna + String? get profileImageUrl => + userProfile['role_data']?['foto_profil'] ?? + _authController.roleData?.fotoProfil; + // Getter untuk counter dari CounterService RxInt get jumlahMenunggu => _counterService.jumlahMenunggu; RxInt get jumlahDiproses => _counterService.jumlahDiproses; diff --git a/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart index f4f2de7..d60bd93 100644 --- a/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart +++ b/lib/app/modules/petugas_desa/views/daftar_donatur_view.dart @@ -263,7 +263,7 @@ class DaftarDonaturView extends GetView { child: Row( children: [ const Icon( - Icons.attach_money, + Icons.payment_rounded, size: 14, color: Colors.green, ), diff --git a/lib/app/modules/petugas_desa/views/dashboard_view.dart b/lib/app/modules/petugas_desa/views/dashboard_view.dart index dc5dccf..65748ce 100644 --- a/lib/app/modules/petugas_desa/views/dashboard_view.dart +++ b/lib/app/modules/petugas_desa/views/dashboard_view.dart @@ -18,195 +18,394 @@ class DashboardView extends GetView { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - return RefreshIndicator( - onRefresh: () => controller.refreshData(), - child: Obx(() => AnimationLimiter( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: controller.isLoading.value - ? const Center(child: CircularProgressIndicator()) - : AnimationConfiguration.staggeredList( - position: 0, - delay: const Duration(milliseconds: 100), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header DisalurKita dengan logo dan slogan - FadeInAnimation( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Image.asset( - 'assets/images/logo-disalurkita.png', - width: 50, - height: 50, - ), - const SizedBox(width: 15), - const Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'DisalurKita', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF1565C0), - ), - ), - SizedBox(height: 5), - Text( - 'Salurkan dengan Pasti, Pantau dengan Bukti', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], + return Scaffold( + body: RefreshIndicator( + onRefresh: () => controller.refreshData(), + child: Obx(() => AnimationLimiter( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: controller.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : AnimationConfiguration.staggeredList( + position: 0, + delay: const Duration(milliseconds: 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan greeting yang lebih menarik + FadeInAnimation( + child: GreetingHeader( + name: controller.namaLengkap, + role: 'Petugas Desa', + desa: controller.desa, + nip: controller.nip, + profileImageUrl: controller.profileImageUrl, ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 24), - // Header dengan greeting - FadeInAnimation( - child: GreetingHeader( - name: controller.namaLengkap, - role: 'Petugas Desa', - desa: controller.desa, + // Rangkuman Statistik + FadeInAnimation( + delay: const Duration(milliseconds: 250), + child: _buildStatistikRangkuman(), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 24), - // Jadwal penyaluran hari ini - FadeInAnimation( - delay: const Duration(milliseconds: 300), - child: _buildJadwalHariIni(), - ), - const SizedBox(height: 20), + // Progress penyaluran + FadeInAnimation( + delay: const Duration(milliseconds: 300), + child: _buildProgressPenyaluran(), + ), + const SizedBox(height: 24), - // Progress penyaluran - FadeInAnimation( - delay: const Duration(milliseconds: 400), - child: _buildProgressPenyaluran(), - ), - const SizedBox(height: 20), + // Jadwal penyaluran hari ini + FadeInAnimation( + delay: const Duration(milliseconds: 350), + child: _buildJadwalHariIni(), + ), + const SizedBox(height: 24), - // Statistik performa desa - FadeInAnimation( - delay: const Duration(milliseconds: 500), - child: _buildStatistikPerforma(), - ), - const SizedBox(height: 20), + // Grafik Penyaluran Bantuan + FadeInAnimation( + delay: const Duration(milliseconds: 400), + child: _buildGrafikPenyaluran(), + ), + const SizedBox(height: 24), - // Daftar penerima terbaru - FadeInAnimation( - delay: const Duration(milliseconds: 600), - child: _buildRecipientsList(textTheme), - ), - ], + // Statistik performa desa + FadeInAnimation( + delay: const Duration(milliseconds: 450), + child: _buildStatistikPerforma(), + ), + const SizedBox(height: 24), + + // Daftar penerima terbaru + FadeInAnimation( + delay: const Duration(milliseconds: 500), + child: _buildRecipientsList(textTheme), + ), + const SizedBox(height: 16), + ], + ), ), - ), + ), + ), + )), + ), + ); + } + + Widget _buildActionButton( + String title, IconData icon, Color color, VoidCallback onTap) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + width: 75, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withOpacity(0.3), width: 1), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 28), + const SizedBox(height: 8), + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: color, ), ), - )), + ], + ), + ), + ); + } + + Widget _buildStatistikRangkuman() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ringkasan Kegiatan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'Total Penerima', + controller.totalPenerima.value.toString(), + Icons.people, + Colors.blue, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatItem( + 'Penyaluran', + controller.totalPenyaluran.value.toString(), + Icons.calendar_month, + Colors.green, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'Pengaduan', + controller.jumlahDiproses.value.toString(), + Icons.warning_amber, + Colors.orange, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatItem( + 'Penitipan', + controller.totalPenitipanTerverifikasi.value.toString(), + Icons.inventory, + Colors.purple, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildStatItem( + String title, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 22), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + Text( + title, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), ); } Widget _buildJadwalHariIni() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Jadwal Penyaluran Hari Ini', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - ), - const SizedBox(height: 12), - FutureBuilder>?>( - future: SupabaseService.to.getJadwalAktif(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return const Center(child: Text('Gagal memuat jadwal')); - } - - final jadwalList = snapshot.data; - - if (jadwalList == null || jadwalList.isEmpty) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(12), - ), - child: const Row( - children: [ - Icon(Icons.event_busy, color: Colors.grey), - SizedBox(width: 8), - Text('Tidak ada jadwal penyaluran hari ini'), - ], - ), - ); - } - - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: jadwalList.length, - itemBuilder: (context, index) { - final jadwal = jadwalList[index]; - final DateTime tanggal = - DateTime.parse(jadwal['tanggal_penyaluran']); - final String formattedDate = - FormatHelper.formatDateTime(tanggal); - final kategoriBantuan = - jadwal['kategori_bantuan'] as Map; - final lokasiPenyaluran = - jadwal['lokasi_penyaluran'] as Map; - - return Column( - children: [ - if (index > 0) const SizedBox(height: 10), - ScheduleCard( - title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran', - location: - lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia', - dateTime: formattedDate, - isToday: true, - onTap: () => Get.toNamed(Routes.detailPenyaluran, - parameters: {'id': jadwal['id']}), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, ), - ], + child: Icon( + Icons.event_note, + color: AppTheme.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 10), + Text( + 'Jadwal Penyaluran Hari Ini', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + FutureBuilder>?>( + future: SupabaseService.to.getJadwalAktif(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ), ); - }, - ); - }, - ), - ], + } + + if (snapshot.hasError) { + return Container( + height: 100, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, color: Colors.red, size: 28), + SizedBox(height: 8), + Text( + 'Gagal memuat jadwal', + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + ); + } + + final jadwalList = snapshot.data; + + if (jadwalList == null || jadwalList.isEmpty) { + return Container( + height: 100, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.blue.withOpacity(0.1), + width: 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event_busy, + color: Colors.blue.shade300, + size: 32, + ), + const SizedBox(height: 8), + const Text( + 'Tidak ada jadwal penyaluran hari ini', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: jadwalList.length, + itemBuilder: (context, index) { + final jadwal = jadwalList[index]; + final DateTime tanggal = + DateTime.parse(jadwal['tanggal_penyaluran']); + final String formattedDate = + FormatHelper.formatDateTime(tanggal); + final kategoriBantuan = + jadwal['kategori_bantuan'] as Map; + final lokasiPenyaluran = + jadwal['lokasi_penyaluran'] as Map; + + return Column( + children: [ + if (index > 0) const SizedBox(height: 10), + ScheduleCard( + title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran', + location: + lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia', + dateTime: formattedDate, + isToday: true, + onTap: () => Get.toNamed(Routes.detailPenyaluran, + parameters: {'id': jadwal['id']}), + ), + ], + ); + }, + ); + }, + ), + ], + ), ); } @@ -219,25 +418,56 @@ class DashboardView extends GetView { final total = terlaksana + batal + dijadwalkan + aktif; final progressValue = total > 0 ? (terlaksana + batal) / total : 0.0; - final belumTerlaksana = dijadwalkan + - aktif; // Yang belum terlaksana adalah yang dijadwalkan dan aktif + final belumTerlaksana = dijadwalkan + aktif; return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, + gradient: LinearGradient( + colors: [Colors.indigo.shade700, Colors.indigo.shade500], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.indigo.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Progress Penyaluran', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Progress Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'Total: $total Penyaluran', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ], ), const SizedBox(height: 20), Row( @@ -246,13 +476,25 @@ class DashboardView extends GetView { radius: 60.0, lineWidth: 10.0, percent: progressValue > 1.0 ? 1.0 : progressValue, - center: Text( - '${(progressValue * 100).toInt()}%', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + center: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${(progressValue * 100).toInt()}%', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Text( + 'Selesai', + style: TextStyle( + fontSize: 12, + color: Colors.white70, + ), + ), + ], ), progressColor: Colors.white, backgroundColor: Colors.white.withOpacity(0.2), @@ -265,28 +507,25 @@ class DashboardView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildProgressDetailItem( + _buildProgressDetailItemNew( + Icons.check_circle, 'Telah Terlaksana', '$terlaksana', - Colors.white, + Colors.green.shade300, ), - const SizedBox(height: 8), - _buildProgressDetailItem( + const SizedBox(height: 12), + _buildProgressDetailItemNew( + Icons.pending_actions, 'Belum Terlaksana', '$belumTerlaksana', - Colors.white.withOpacity(0.7), + Colors.amber.shade300, ), - const SizedBox(height: 8), - _buildProgressDetailItem( + const SizedBox(height: 12), + _buildProgressDetailItemNew( + Icons.cancel, 'Dibatalkan', '$batal', - Colors.white.withOpacity(0.7), - ), - const SizedBox(height: 8), - _buildProgressDetailItem( - 'Total Penyaluran', - '$total', - Colors.white, + Colors.red.shade300, ), ], ), @@ -298,23 +537,281 @@ class DashboardView extends GetView { ); } - Widget _buildProgressDetailItem(String label, String value, Color color) { + Widget _buildProgressDetailItemNew( + IconData icon, String label, String value, Color iconColor) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - label, - style: TextStyle( - fontSize: 14, - color: color, + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: iconColor, size: 16), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), ), ), Text( value, - style: TextStyle( - fontSize: 14, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ); + } + + Widget _buildGrafikPenyaluran() { + final terlaksana = controller.penyaluranTerlaksana.value; + final batal = controller.penyaluranBatal.value; + final dijadwalkan = controller.penyaluranDijadwalkan.value; + final aktif = controller.penyaluranAktif.value; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.insert_chart, + color: AppTheme.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 10), + Text( + 'Status Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + TextButton( + onPressed: () => Get.toNamed(Routes.riwayatPenyaluran), + child: Text( + 'Lihat Riwayat', + style: TextStyle( + fontSize: 12, + color: AppTheme.primaryColor, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: _buildStatusItem( + 'Terlaksana', + terlaksana.toString(), + Icons.check_circle, + Colors.green, + ), + ), + Expanded( + child: _buildStatusItem( + 'Dijadwalkan', + dijadwalkan.toString(), + Icons.event, + Colors.blue, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildStatusItem( + 'Aktif', + aktif.toString(), + Icons.play_circle, + Colors.orange, + ), + ), + Expanded( + child: _buildStatusItem( + 'Dibatalkan', + batal.toString(), + Icons.cancel, + Colors.red, + ), + ), + ], + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: 10, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + if (terlaksana > 0) + Flexible( + flex: terlaksana, + child: Container( + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + if (aktif > 0) + Flexible( + flex: aktif, + child: Container( + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: aktif > 0 && terlaksana == 0 + ? BorderRadius.circular(5) + : const BorderRadius.only( + topRight: Radius.circular(5), + bottomRight: Radius.circular(5), + ), + ), + ), + ), + if (dijadwalkan > 0) + Flexible( + flex: dijadwalkan, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: + dijadwalkan > 0 && terlaksana == 0 && aktif == 0 + ? BorderRadius.circular(5) + : const BorderRadius.only( + topRight: Radius.circular(5), + bottomRight: Radius.circular(5), + ), + ), + ), + ), + if (batal > 0) + Flexible( + flex: batal, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(5), + bottomRight: Radius.circular(5), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem('Terlaksana', Colors.green), + const SizedBox(width: 12), + _buildLegendItem('Aktif', Colors.orange), + const SizedBox(width: 12), + _buildLegendItem('Dijadwalkan', Colors.blue), + const SizedBox(width: 12), + _buildLegendItem('Batal', Colors.red), + ], + ), + ], + ), + ); + } + + Widget _buildStatusItem( + String label, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3), width: 1), + ), + child: Column( + children: [ + Icon(icon, color: color, size: 28), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + Widget _buildLegendItem(String label, Color color) { + return Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontSize: 10, + color: Colors.grey, ), ), ], @@ -322,225 +819,429 @@ class DashboardView extends GetView { } Widget _buildStatistikPerforma() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Statistik Performa', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: StatisticCard( - title: 'Penitipan', - count: controller.jumlahMenunggu.value.toString(), - subtitle: 'Perlu Konfirmasi', - height: 120, - icon: Icons.inbox, - gradient: LinearGradient( - colors: [Colors.orange, Colors.deepOrange], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.analytics, + color: AppTheme.primaryColor, + size: 20, ), ), - ), - const SizedBox(width: 10), - Expanded( - child: StatisticCard( - title: 'Pengaduan', - count: controller.jumlahDiproses.value.toString(), - subtitle: 'Perlu Tindakan', - height: 120, - gradient: LinearGradient( - colors: [Colors.orange, Colors.deepOrange], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + const SizedBox(width: 10), + Text( + 'Statistik Performa', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, ), - icon: Icons.warning_amber, ), - ), - ], - ), - ], + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: StatisticCard( + title: 'Penitipan', + count: controller.jumlahMenunggu.value.toString(), + subtitle: 'Perlu Konfirmasi', + height: 120, + icon: Icons.inbox, + gradient: LinearGradient( + colors: [Colors.orange.shade600, Colors.orange.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: StatisticCard( + title: 'Penerima', + count: controller.totalPenerima.value.toString(), + subtitle: 'Terdaftar', + height: 120, + gradient: LinearGradient( + colors: [Colors.blue.shade600, Colors.blue.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + icon: Icons.people, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: StatisticCard( + title: 'Penyaluran', + count: controller.penyaluranTerlaksana.value.toString(), + subtitle: 'Terlaksana', + height: 120, + gradient: LinearGradient( + colors: [Colors.green.shade600, Colors.green.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + icon: Icons.assignment_turned_in, + ), + ), + const SizedBox(width: 12), + Expanded( + child: StatisticCard( + title: 'Pengaduan', + count: controller.jumlahDiproses.value.toString(), + subtitle: 'Memerlukan Tindakan', + height: 120, + gradient: LinearGradient( + colors: [Colors.red.shade600, Colors.red.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + icon: Icons.warning_amber, + ), + ), + ], + ), + ], + ), ); } Widget _buildRecipientsList(TextTheme textTheme) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Daftar Penerima Terbaru', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - TextButton( - onPressed: () { - Get.toNamed(Routes.daftarPenerima); - }, - child: Row( - children: [ - Text( - 'Lihat Semua', - style: textTheme.bodyMedium?.copyWith( - color: AppTheme.primaryColor, - ), - ), - Icon( - Icons.chevron_right, - size: 16, - color: AppTheme.primaryColor, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 10), - FutureBuilder>?>( - future: SupabaseService.to.getPenerimaTerbaru(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return const Center(child: Text('Gagal memuat data penerima')); - } - - final penerimaList = snapshot.data; - - if (penerimaList == null || penerimaList.isEmpty) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(12), - ), - child: const Row( - children: [ - Icon(Icons.person_off, color: Colors.grey), - SizedBox(width: 8), - Text('Belum ada data penerima'), - ], - ), - ); - } - - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: penerimaList.length > 3 ? 3 : penerimaList.length, - itemBuilder: (context, index) { - final penerima = penerimaList[index]; - final name = penerima['nama_lengkap'] ?? 'Nama tidak tersedia'; - final nik = penerima['nik'] ?? 'NIK tidak tersedia'; - final status = penerima['status'] ?? 'AKTIF'; - final id = penerima['id'] ?? 'ID tidak tersedia'; - final fotoProfil = penerima['foto_profil'] ?? null; - - return _buildRecipientItem( - name, nik, status, id, textTheme, fotoProfil); - }, - ); - }, - ), - ], - ); - } - - Widget _buildRecipientItem(String name, String nik, String status, String id, - TextTheme textTheme, String? fotoProfil) { return Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, - borderRadius: BorderRadius.circular(12), + color: Colors.white, + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), - blurRadius: 5, - offset: const Offset(0, 2), + blurRadius: 10, + offset: const Offset(0, 4), ), ], ), - child: InkWell( - onTap: () { - Get.toNamed(Routes.detailPenerima, arguments: id); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - CircleAvatar( - backgroundColor: Colors.white.withOpacity(0.2), - backgroundImage: - fotoProfil != null && fotoProfil.toString().isNotEmpty - ? NetworkImage(fotoProfil) - : null, - child: (fotoProfil == null || fotoProfil.toString().isEmpty) - ? Text( - name.toString().substring(0, 1).toUpperCase(), - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: 24, - ), - ) - : null, - ), - const SizedBox(width: 12), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - name, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.people, + color: AppTheme.primaryColor, + size: 20, ), ), - Text( - 'NIK: $nik', - style: textTheme.bodyMedium?.copyWith( - color: Colors.white.withOpacity(0.8), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Daftar Penerima', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + overflow: TextOverflow.ellipsis, ), ), ], ), ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - status, - style: textTheme.bodySmall?.copyWith( - color: Colors.white, - fontSize: 12, + InkWell( + onTap: () => Get.toNamed(Routes.daftarPenerima), + borderRadius: BorderRadius.circular(50), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(50), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Lihat Semua', + style: TextStyle( + fontSize: 12, + color: AppTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 2), + Icon( + Icons.arrow_forward, + size: 12, + color: AppTheme.primaryColor, + ), + ], ), ), ), ], ), - ), + const SizedBox(height: 16), + FutureBuilder>?>( + future: SupabaseService.to.getPenerimaTerbaru(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + return Container( + height: 100, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, color: Colors.red, size: 28), + SizedBox(height: 8), + Text( + 'Gagal memuat data penerima', + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + ); + } + + final penerimaList = snapshot.data; + + if (penerimaList == null || penerimaList.isEmpty) { + return Container( + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.person_off, color: Colors.grey, size: 28), + SizedBox(height: 8), + Text( + 'Belum ada data penerima', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: penerimaList.length > 3 ? 3 : penerimaList.length, + itemBuilder: (context, index) { + final penerima = penerimaList[index]; + final name = + penerima['nama_lengkap'] ?? 'Nama tidak tersedia'; + final nik = penerima['nik'] ?? 'NIK tidak tersedia'; + final status = penerima['status'] ?? 'AKTIF'; + final id = penerima['id'] ?? 'ID tidak tersedia'; + final fotoProfil = penerima['foto_profil'] ?? null; + + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade700, Colors.blue.shade500], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.2), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Get.toNamed(Routes.detailPenerima, arguments: id); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 25, + backgroundColor: + Colors.white.withOpacity(0.2), + backgroundImage: fotoProfil != null && + fotoProfil.toString().isNotEmpty + ? NetworkImage(fotoProfil) + : null, + child: (fotoProfil == null || + fotoProfil.toString().isEmpty) + ? Text( + name + .toString() + .substring(0, 1) + .toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 24, + ), + ) + : null, + ), + Positioned( + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: status == 'AKTIF' + ? Colors.green + : Colors.red, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 1.5, + ), + ), + width: 12, + height: 12, + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + Icons.credit_card, + color: Colors.white70, + size: 14, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'NIK: $nik', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + status == 'AKTIF' + ? Icons.check_circle + : Icons.cancel, + color: status == 'AKTIF' + ? Colors.green.shade300 + : Colors.red.shade300, + size: 12, + ), + const SizedBox(width: 4), + Text( + status, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ], ), ); } diff --git a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart index b680308..063ca3c 100644 --- a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart @@ -277,7 +277,7 @@ class DetailDonaturView extends GetView { child: _buildSummaryCard( title: 'Donasi Uang', value: jumlahDonasiUang.toString(), - icon: Icons.attach_money, + icon: Icons.payment_rounded, color: jenisColor, ), ), diff --git a/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart b/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart index c61eb45..2f0b9a8 100644 --- a/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart @@ -5,14 +5,10 @@ import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart'; -import 'package:penyaluran_app/app/widgets/cards/info_card.dart'; -import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:timeline_tile/timeline_tile.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; -import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart'; -import 'package:penyaluran_app/app/widgets/inputs/text_input.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:penyaluran_app/app/widgets/widgets.dart'; @@ -1391,61 +1387,9 @@ class DetailPengaduanView extends GetView { ? List.from(tindakan.buktiTindakan!) : []; - // Fungsi untuk memilih bukti tindakan - Future pickBuktiTindakan( - BuildContext dialogContext, bool fromCamera) async { - try { - final ImagePicker picker = ImagePicker(); - final XFile? pickedFile = await picker.pickImage( - source: fromCamera ? ImageSource.camera : ImageSource.gallery, - imageQuality: 80, - maxWidth: 1200, - maxHeight: 1200, - preferredCameraDevice: - fromCamera ? CameraDevice.rear : CameraDevice.front, - ); - - if (pickedFile != null) { - // Tampilkan loading dialog - showDialog( - context: dialogContext, - barrierDismissible: false, - builder: (BuildContext context) { - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - - try { - // Tambahkan gambar ke daftar - buktiTindakanPaths.add(pickedFile.path); - - // Tutup loading dialog - Navigator.of(dialogContext, rootNavigator: true).pop(); - - // Tutup dialog pilih sumber foto - Navigator.of(dialogContext).pop(); - } catch (e) { - // Tutup loading dialog jika terjadi error - Navigator.of(dialogContext, rootNavigator: true).pop(); - rethrow; - } - } - } catch (e) { - print('Error picking image: $e'); - Get.snackbar( - 'Error', - 'Gagal mengambil gambar: ${e.toString()}', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - } - // Fungsi untuk menampilkan dialog pilih sumber foto - void showPilihSumberFoto(BuildContext dialogContext) { + void showPilihSumberFoto( + BuildContext dialogContext, Function(BuildContext, bool) pickFunction) { showDialog( context: dialogContext, builder: (innerContext) => AlertDialog( @@ -1456,12 +1400,12 @@ class DetailPengaduanView extends GetView { ListTile( leading: const Icon(Icons.camera_alt), title: const Text('Kamera'), - onTap: () => pickBuktiTindakan(innerContext, true), + onTap: () => pickFunction(innerContext, true), ), ListTile( leading: const Icon(Icons.photo_library), title: const Text('Galeri'), - onTap: () => pickBuktiTindakan(innerContext, false), + onTap: () => pickFunction(innerContext, false), ), ], ), @@ -1473,6 +1417,61 @@ class DetailPengaduanView extends GetView { context: context, builder: (dialogContext) => StatefulBuilder(builder: (stateContext, setState) { + // Fungsi untuk memilih bukti tindakan dipindahkan ke dalam StatefulBuilder + Future pickBuktiTindakan( + BuildContext innerContext, bool fromCamera) async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? pickedFile = await picker.pickImage( + source: fromCamera ? ImageSource.camera : ImageSource.gallery, + imageQuality: 80, + maxWidth: 1200, + maxHeight: 1200, + preferredCameraDevice: + fromCamera ? CameraDevice.rear : CameraDevice.front, + ); + + if (pickedFile != null) { + // Tampilkan loading dialog + showDialog( + context: innerContext, + barrierDismissible: false, + builder: (BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + }, + ); + + try { + // Tambahkan gambar ke daftar dan update state + setState(() { + buktiTindakanPaths.add(pickedFile.path); + }); + + // Tutup loading dialog + Navigator.of(innerContext, rootNavigator: true).pop(); + + // Tutup dialog pilih sumber foto + Navigator.of(innerContext).pop(); + } catch (e) { + // Tutup loading dialog jika terjadi error + Navigator.of(innerContext, rootNavigator: true).pop(); + rethrow; + } + } + } catch (e) { + print('Error picking image: $e'); + Get.snackbar( + 'Error', + 'Gagal mengambil gambar: ${e.toString()}', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + return AlertDialog( title: Row( children: [ @@ -1581,7 +1580,8 @@ class DetailPengaduanView extends GetView { children: [ if (buktiTindakanPaths.isEmpty) InkWell( - onTap: () => showPilihSumberFoto(stateContext), + onTap: () => showPilihSumberFoto( + stateContext, pickBuktiTindakan), child: Container( height: 150, width: double.infinity, @@ -1619,15 +1619,16 @@ class DetailPengaduanView extends GetView { height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: buktiTindakanPaths - .length, //tombol tambah jika tidak selesai + itemCount: buktiTindakanPaths.length + + 1, // Tambah 1 untuk tombol tambah itemBuilder: (context, index) { if (index == buktiTindakanPaths.length) { // Tombol tambah foto return InkWell( onTap: () => showPilihSumberFoto( - stateContext), + stateContext, + pickBuktiTindakan), child: Container( width: 100, margin: const EdgeInsets.only( diff --git a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart index 3d86bae..08a3b4b 100644 --- a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart +++ b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart @@ -675,7 +675,7 @@ class _KonfirmasiPenerimaPageState extends State { borderRadius: BorderRadius.circular(6), ), child: Icon( - isUang ? Icons.attach_money : Icons.scale_outlined, + isUang ? Icons.payment_rounded : Icons.scale_outlined, color: Colors.green, size: 20, ), diff --git a/lib/app/modules/petugas_desa/views/pengaduan_view.dart b/lib/app/modules/petugas_desa/views/pengaduan_view.dart index 651b571..9fee461 100644 --- a/lib/app/modules/petugas_desa/views/pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/pengaduan_view.dart @@ -521,7 +521,15 @@ class PengaduanView extends GetView { ), ), Text( - '${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}', + item.stokBantuan?['is_uang'] == true + ? FormatHelper.formatRupiah( + item.jumlahBantuan is num + ? item.jumlahBantuan + : double.tryParse(item + .jumlahBantuan + .toString()) ?? + 0) + : '${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}', style: TextStyle( fontWeight: FontWeight.w500, color: Colors.grey.shade800, diff --git a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart index f11c00d..7ba0677 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -10,6 +10,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dar import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart'; +import 'package:penyaluran_app/app/widgets/app_drawer.dart'; class PetugasDesaView extends GetView { const PetugasDesaView({super.key}); @@ -190,360 +191,119 @@ class PetugasDesaView extends GetView { } 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: Offset(0, 5), - ), - ], - ), - child: Hero( - tag: 'profile-photo', - 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) - ? Text( - controller.nama.isNotEmpty - ? controller.nama - .substring(0, 1) - .toUpperCase() - : '?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - fontSize: 30, - ), - ) - : null, - ), - ), - ), - SizedBox(height: 16), - Text( - 'Halo,', - style: TextStyle( - color: Colors.white70, - fontSize: 16, - ), - ), - Text( - controller.nama, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 22, - ), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - controller.formattedRole, - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - SizedBox(width: 8), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.location_on, - color: Colors.white, - size: 14, - ), - SizedBox(width: 4), - Text( - controller.desa, - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ], - ), + return Obx(() { + Map> menuCategories = { + 'Menu Utama': [ + DrawerMenuItem( + icon: Icons.dashboard_outlined, + activeIcon: Icons.dashboard, + title: 'Dashboard', + isSelected: controller.activeTabIndex.value == 0, + onTap: () { + controller.activeTabIndex.value = 0; + }, ), - Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: [ - _buildMenuCategory('Menu Utama'), - Obx(() => _buildMenuItem( - icon: Icons.dashboard_outlined, - activeIcon: Icons.dashboard, - title: 'Dashboard', - isSelected: controller.activeTabIndex.value == 0, - onTap: () { - Navigator.pop(context); - controller.changeTab(0); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.handshake_outlined, - activeIcon: Icons.handshake, - title: 'Penyaluran', - isSelected: controller.activeTabIndex.value == 1, - onTap: () { - Navigator.pop(context); - controller.changeTab(1); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.inventory_2_outlined, - activeIcon: Icons.inventory_2, - title: 'Penitipan', - isSelected: controller.activeTabIndex.value == 2, - onTap: () { - Navigator.pop(context); - controller.changeTab(2); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.warning_amber_outlined, - activeIcon: Icons.warning_amber, - title: 'Pengaduan', - isSelected: controller.activeTabIndex.value == 3, - onTap: () { - Navigator.pop(context); - controller.changeTab(3); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.inventory_outlined, - activeIcon: Icons.inventory, - title: 'Stok Bantuan', - isSelected: controller.activeTabIndex.value == 4, - onTap: () { - Navigator.pop(context); - controller.changeTab(4); - }, - )), - _buildMenuCategory('Kelola Data'), - _buildMenuItem( - icon: Icons.person_add_outlined, - activeIcon: Icons.person_add, - title: 'Kelola Penerima', - onTap: () { - Navigator.pop(context); - Get.toNamed('/daftar-penerima'); - }, - ), - _buildMenuItem( - icon: Icons.people_outlined, - activeIcon: Icons.people, - title: 'Kelola Donatur', - onTap: () { - Navigator.pop(context); - Get.toNamed('/daftar-donatur'); - }, - ), - _buildMenuItem( - icon: Icons.location_on_outlined, - activeIcon: Icons.location_on, - title: 'Lokasi Penyaluran', - onTap: () { - Navigator.pop(context); - Get.toNamed('/lokasi-penyaluran'); - }, - ), - _buildMenuItem( - icon: Icons.description_outlined, - activeIcon: Icons.description, - title: 'Laporan Penyaluran', - onTap: () { - Navigator.pop(context); - Get.toNamed('/laporan-penyaluran'); - }, - ), - _buildMenuCategory('Pengaturan'), - _buildMenuItem( - icon: Icons.person_outline, - activeIcon: Icons.person, - title: 'Profil', - onTap: () { - Navigator.pop(context); - Get.toNamed('/profile'); - }, - ), - const Divider(), - _buildMenuItem( - icon: Icons.info_outline, - activeIcon: Icons.info, - title: 'Tentang Kami', - onTap: () { - Navigator.pop(context); - Get.toNamed('/about'); - }, - ), - _buildMenuItem( - icon: Icons.logout, - title: 'Keluar', - onTap: () { - Navigator.pop(context); - controller.logout(); - }, - isLogout: true, - ), - ], - ), + DrawerMenuItem( + icon: Icons.volunteer_activism_outlined, + activeIcon: Icons.volunteer_activism, + title: 'Penyaluran', + isSelected: controller.activeTabIndex.value == 1, + badgeCount: controller.jumlahMenunggu.value > 0 + ? controller.jumlahMenunggu.value + : null, + badgeColor: Colors.green, + onTap: () { + controller.activeTabIndex.value = 1; + }, ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - '© ${DateTime.now().year} DisalurKita', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), + DrawerMenuItem( + icon: Icons.inbox_outlined, + activeIcon: Icons.inbox, + title: 'Penitipan', + isSelected: controller.activeTabIndex.value == 2, + onTap: () { + controller.activeTabIndex.value = 2; + }, + ), + DrawerMenuItem( + icon: Icons.report_problem_outlined, + activeIcon: Icons.report_problem, + title: 'Pengaduan', + isSelected: controller.activeTabIndex.value == 3, + badgeCount: controller.jumlahDiproses.value > 0 + ? controller.jumlahDiproses.value + : null, + badgeColor: Colors.orange, + onTap: () { + controller.activeTabIndex.value = 3; + }, + ), + DrawerMenuItem( + icon: Icons.inventory_outlined, + activeIcon: Icons.inventory, + title: 'Stok Bantuan', + isSelected: controller.activeTabIndex.value == 4, + onTap: () { + controller.activeTabIndex.value = 4; + }, ), ], - ), - ); - } - - Widget _buildMenuCategory(String title) { - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), - child: Text( - title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ); - } - - Widget _buildMenuItem({ - required IconData icon, - IconData? activeIcon, - required String title, - bool isSelected = false, - String? badge, - required Function() onTap, - bool isLogout = false, - }) { - return AnimatedContainer( - duration: Duration(milliseconds: 200), - decoration: BoxDecoration( - color: isSelected - ? AppTheme.primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - margin: EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: ListTile( - leading: Stack( - alignment: Alignment.center, - children: [ - Icon( - isSelected ? (activeIcon ?? icon) : icon, - color: isSelected - ? AppTheme.primaryColor - : isLogout - ? Colors.red - : Colors.grey[700], - size: 24, - ), - if (badge != null) - Positioned( - top: 0, - right: 0, - child: Container( - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - constraints: BoxConstraints( - minWidth: 16, - minHeight: 16, - ), - child: Text( - badge, - style: TextStyle( - fontSize: 10, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), + 'Pengaturan': [ + DrawerMenuItem( + icon: Icons.notifications_outlined, + activeIcon: Icons.notifications, + title: 'Notifikasi', + badgeCount: controller.jumlahNotifikasiBelumDibaca.value > 0 + ? controller.jumlahNotifikasiBelumDibaca.value + : null, + badgeColor: Colors.red, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NotifikasiView(), ), - ), - ], - ), - title: Text( - title, - style: TextStyle( - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: isSelected - ? AppTheme.primaryColor - : isLogout - ? Colors.red - : Colors.grey[800], - fontSize: 14, + ); + }, ), - ), - onTap: onTap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - visualDensity: VisualDensity.compact, - selectedTileColor: AppTheme.primaryColor.withOpacity(0.1), - selected: isSelected, - ), - ); + DrawerMenuItem( + icon: Icons.person_outline, + activeIcon: Icons.person, + title: 'Profil', + onTap: () { + Get.toNamed('/profile'); + }, + ), + DrawerMenuItem( + icon: Icons.info_outline, + activeIcon: Icons.info, + title: 'Tentang Kami', + onTap: () { + Get.toNamed('/about'); + }, + ), + DrawerMenuItem( + icon: Icons.logout, + title: 'Keluar', + isLogout: true, + onTap: () { + controller.logout(); + }, + ), + ], + }; + + return AppDrawer( + nama: controller.namaLengkap, + role: 'Petugas Desa', + desa: controller.desa, + avatar: controller.profilePhotoUrl, + menuItems: const [], // Tidak digunakan karena menggunakan menuCategories + menuCategories: menuCategories, + onLogout: controller.logout, + footerText: '© ${DateTime.now().year} DisalurKita', + ); + }); } Widget _buildBottomNavigationBar() { diff --git a/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart b/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart index fed5b74..4c93997 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart @@ -347,10 +347,16 @@ class RiwayatStokView extends GetView { ), ), ...controller.daftarStokBantuan.map((stok) { + final bool isUang = stok.isUang ?? false; + final String formattedJumlah = isUang + ? FormatHelper.formatRupiah( + stok.totalStok ?? 0) + : '${stok.totalStok} ${stok.satuan}'; + return DropdownMenuItem( value: stok.id, child: Text( - stok.nama ?? '-', + '${stok.nama ?? '-'} ($formattedJumlah)', overflow: TextOverflow.ellipsis, ), ); @@ -380,6 +386,9 @@ class RiwayatStokView extends GetView { : 'Tidak diketahui'; final stokBantuanSatuan = riwayat.stokBantuan != null ? riwayat.stokBantuan!['satuan'] ?? '' : ''; + final bool isUang = riwayat.stokBantuan != null + ? riwayat.stokBantuan!['is_uang'] ?? false + : false; final sumberLabels = { 'penitipan': 'Penitipan', 'penerimaan': 'Penerimaan', @@ -464,7 +473,9 @@ class RiwayatStokView extends GetView { ), const SizedBox(width: 4), Text( - '${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan', + isUang + ? FormatHelper.formatRupiah(riwayat.jumlah ?? 0) + : '${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan', style: TextStyle( fontWeight: FontWeight.bold, color: isPenambahan ? Colors.green : Colors.red, @@ -761,10 +772,13 @@ class RiwayatStokView extends GetView { value: controller.selectedStokBantuan.value, items: controller.daftarStokBantuan .map((StokBantuanModel stok) { + final bool isUang = stok.isUang ?? false; + final String formattedStok = isUang + ? FormatHelper.formatRupiah(stok.totalStok ?? 0) + : '${stok.totalStok} ${stok.satuan}'; return DropdownMenuItem( value: stok, - child: Text( - '${stok.nama} (${stok.totalStok} ${stok.satuan})'), + child: Text('${stok.nama} ($formattedStok)'), ); }).toList(), onChanged: (StokBantuanModel? value) { diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 7232b3f..7c6d9ef 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -24,7 +24,8 @@ class StokBantuanView extends GetView { }, backgroundColor: AppTheme.primaryColor, icon: const Icon(Icons.add, color: Colors.white), - label: const Text('Tambah Stok', style: TextStyle(color: Colors.white)), + label: const Text('Tambah Jenis Stok', + style: TextStyle(color: Colors.white)), elevation: 2, ), ); @@ -636,7 +637,7 @@ class StokBantuanView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Tambah Stok Bantuan', + 'Tambah Jenis Stok Bantuan', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart index b62c49c..af1e914 100644 --- a/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart @@ -363,7 +363,7 @@ class TambahPenyaluranView extends GetView { child: Row( children: [ isUang.value - ? const Icon(Icons.attach_money) + ? const Icon(Icons.payment_rounded) : const Icon(Icons.inventory_2), const SizedBox(width: 8), Expanded( diff --git a/lib/app/modules/warga/views/detail_pengaduan_view.dart b/lib/app/modules/warga/views/detail_pengaduan_view.dart index 5e5d3ea..cf399dc 100644 --- a/lib/app/modules/warga/views/detail_pengaduan_view.dart +++ b/lib/app/modules/warga/views/detail_pengaduan_view.dart @@ -7,8 +7,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:timeline_tile/timeline_tile.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart'; -import 'package:penyaluran_app/app/widgets/cards/info_card.dart'; import 'dart:io'; import 'package:penyaluran_app/app/widgets/widgets.dart'; @@ -652,6 +650,76 @@ class WargaDetailPengaduanView extends GetView { ), ), ), + + // Menampilkan foto pengaduan jika ada + if (pengaduan.fotoPengaduan != null && + pengaduan.fotoPengaduan!.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'Foto Pengaduan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Container( + height: 120, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: pengaduan.fotoPengaduan!.map((foto) { + return GestureDetector( + onTap: () => _showFullScreenImage(context, foto), + child: Container( + width: 120, + height: 120, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + image: DecorationImage( + image: foto.startsWith('http') + ? NetworkImage(foto) + : FileImage(File(foto)) as ImageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + child: const Icon( + Icons.zoom_in, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ], + const SizedBox(height: 16), Row( children: [ @@ -926,7 +994,12 @@ class WargaDetailPengaduanView extends GetView { 'Nama Penyaluran', pengaduan.namaPenyaluran, Icons.assignment), _buildInfoItem( 'Jenis Bantuan', pengaduan.jenisBantuan, Icons.category), - _buildInfoItem('Jumlah Bantuan', pengaduan.jumlahBantuan, + _buildInfoItem( + 'Jumlah Bantuan', + pengaduan.isUang == true + ? FormatHelper.formatRupiah( + double.tryParse(pengaduan.jumlahBantuan ?? '0')) + : pengaduan.jumlahBantuan, Icons.shopping_basket), _buildInfoItem( 'Deskripsi', pengaduan.deskripsiPenyaluran, Icons.description), diff --git a/lib/app/modules/warga/views/warga_dashboard_view.dart b/lib/app/modules/warga/views/warga_dashboard_view.dart index 31a86ba..6f1746d 100644 --- a/lib/app/modules/warga/views/warga_dashboard_view.dart +++ b/lib/app/modules/warga/views/warga_dashboard_view.dart @@ -23,54 +23,6 @@ class WargaDashboardView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header DisalurKita dengan logo dan slogan - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Image.asset( - 'assets/images/logo-disalurkita.png', - width: 50, - height: 50, - ), - const SizedBox(width: 15), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'DisalurKita', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF1565C0), - ), - ), - SizedBox(height: 5), - Text( - 'Salurkan dengan Pasti, Pantau dengan Bukti', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], - ), - ), _buildWelcomeSection(), const SizedBox(height: 24), _buildStatisticSection(), @@ -540,7 +492,7 @@ class WargaDashboardView extends GetView { children: [ if (totalUang > 0) _buildSummaryItem( - icon: Icons.attach_money, + icon: Icons.payment_rounded, color: Colors.green, title: 'Total Bantuan Uang', value: FormatHelper.formatRupiah(totalUang), diff --git a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart index de2d707..f1e525d 100644 --- a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart +++ b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart @@ -158,18 +158,67 @@ class WargaDetailPenerimaanView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // nama penyaluran + Text( + penyaluran.namaPenyaluran ?? 'Nama Penyaluran', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + // deskripsi penyaluran if (penyaluran.deskripsiPenyaluran != null && penyaluran.deskripsiPenyaluran!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text( - penyaluran.deskripsiPenyaluran!, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.description_outlined, + size: 18, + color: Colors.grey.shade600, + ), + const SizedBox(width: 8), + Text( + 'Deskripsi', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + penyaluran.deskripsiPenyaluran!, + style: const TextStyle( + color: Colors.black87, + fontSize: 15, + height: 1.5, + ), + ), + ], ), ), + const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -197,7 +246,7 @@ class WargaDetailPenerimaanView extends GetView { ), child: Icon( penyaluran.isUang == true - ? Icons.attach_money + ? Icons.payment_rounded : Icons.inventory_2, color: penyaluran.isUang == true ? Colors.green @@ -736,13 +785,17 @@ class WargaDetailPenerimaanView extends GetView { // Pastikan menggunakan data terbaru dari model dan cetak ke log untuk debugging final qrData = penyaluran.qrCodeHash ?? 'invalid-qr-code'; print('penyaluran.statusPenyaluran ${penyaluran.statusPenyaluran}'); + print('penyaluran.statusPenerimaan ${penyaluran.statusPenerimaan}'); - // Cek status penyaluran untuk disabled state - final bool isDisabled = penyaluran.statusPenyaluran != null && - (penyaluran.statusPenyaluran!.toUpperCase() == 'DIJADWALKAN' || - penyaluran.statusPenyaluran!.toUpperCase() == 'DISETUJUI' || - penyaluran.statusPenyaluran!.toUpperCase() == 'BATALTERLAKSANA' || - penyaluran.statusPenyaluran!.toUpperCase() == 'TERLAKSANA'); + // Cek status penyaluran dan penerimaan untuk disabled state + final bool isDisabled = (penyaluran.statusPenyaluran != null && + (penyaluran.statusPenyaluran!.toUpperCase() == 'DIJADWALKAN' || + penyaluran.statusPenyaluran!.toUpperCase() == 'DISETUJUI' || + penyaluran.statusPenyaluran!.toUpperCase() == + 'BATALTERLAKSANA' || + penyaluran.statusPenyaluran!.toUpperCase() == 'TERLAKSANA')) || + (penyaluran.statusPenerimaan != null && + penyaluran.statusPenerimaan!.toUpperCase() == 'DITERIMA'); final String statusMessage; if (isDisabled) { @@ -752,6 +805,9 @@ class WargaDetailPenerimaanView extends GetView { } else if (penyaluran.statusPenyaluran!.toUpperCase() == 'TERLAKSANA') { statusMessage = 'QR Code sudah digunakan pada penyaluran yang telah terlaksana'; + } else if (penyaluran.statusPenerimaan != null && + penyaluran.statusPenerimaan!.toUpperCase() == 'DITERIMA') { + statusMessage = 'QR Code sudah digunakan karena bantuan telah diterima'; } else { statusMessage = 'QR Code belum dapat digunakan karena penyaluran belum terlaksana'; diff --git a/lib/app/modules/warga/views/warga_penerimaan_view.dart b/lib/app/modules/warga/views/warga_penerimaan_view.dart index 6eb740f..b4bcf75 100644 --- a/lib/app/modules/warga/views/warga_penerimaan_view.dart +++ b/lib/app/modules/warga/views/warga_penerimaan_view.dart @@ -15,10 +15,6 @@ class WargaPenerimaanView extends GetView { return const Center(child: CircularProgressIndicator()); } - // Debug print untuk melihat jumlah item - print( - 'DEBUG: Jumlah penerimaan tersedia: ${controller.penerimaPenyaluran.length}'); - return RefreshIndicator( onRefresh: () async { // Tambahkan delay untuk memastikan refresh indicator terlihat @@ -102,9 +98,7 @@ class WargaPenerimaanView extends GetView { } Widget _buildPenerimaanList(BuildContext context) { - // Debug print untuk melihat jumlah item - print( - 'DEBUG: Membangun ListView dengan ${controller.penerimaPenyaluran.length} item bantuan'); + // Menggunakan CustomScrollView dan SliverList untuk layout yang lebih stabil // Menggunakan CustomScrollView dan SliverList untuk layout yang lebih stabil return CustomScrollView( @@ -122,9 +116,6 @@ class WargaPenerimaanView extends GetView { final item = controller.penerimaPenyaluran[index]; - // Debug - print('DEBUG: Membangun item $index dengan id: ${item.id}'); - // Menggunakan SizedBox untuk memberikan batas lebar dan tinggi return SizedBox( width: MediaQuery.of(context).size.width, diff --git a/lib/app/modules/warga/views/warga_pengaduan_view.dart b/lib/app/modules/warga/views/warga_pengaduan_view.dart index 882515e..6cecab2 100644 --- a/lib/app/modules/warga/views/warga_pengaduan_view.dart +++ b/lib/app/modules/warga/views/warga_pengaduan_view.dart @@ -289,15 +289,22 @@ class WargaPengaduanView extends GetView { Expanded( child: _buildInfoItem( 'Jenis', - item.jenisBantuan ?? + item.stokBantuan?[ + 'nama'] ?? + item.jenisBantuan ?? "Tidak tersedia", ), ), Expanded( child: _buildInfoItem( 'Jumlah', - item.jumlahBantuan ?? - "Tidak tersedia", + item.isUang + ? FormatHelper.formatRupiah( + double.tryParse(item + .jumlahBantuan + .toString()) ?? + 0) + : '${item.jumlahBantuan} ${item.stokBantuan?['satuan'] ?? ''}', ), ), ], diff --git a/lib/app/modules/warga/views/warga_view.dart b/lib/app/modules/warga/views/warga_view.dart index df4cf89..f195405 100644 --- a/lib/app/modules/warga/views/warga_view.dart +++ b/lib/app/modules/warga/views/warga_view.dart @@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/modules/warga/views/warga_dashboard_view.dart import 'package:penyaluran_app/app/modules/warga/views/warga_penerimaan_view.dart'; import 'package:penyaluran_app/app/modules/warga/views/warga_pengaduan_view.dart'; import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart'; +import 'package:penyaluran_app/app/widgets/app_drawer.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class WargaView extends GetView { @@ -133,276 +134,83 @@ class WargaView extends GetView { } }); - 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: Offset(0, 5), - ), - ], - ), - child: CircleAvatar( - radius: 40, - backgroundColor: Colors.white70, - backgroundImage: controller.fotoProfil.value.isNotEmpty - ? NetworkImage(controller.fotoProfil.value) - : null, - child: controller.fotoProfil.isEmpty - ? Text( - controller.nama.isNotEmpty - ? controller.nama.substring(0, 1).toUpperCase() - : '?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: 24, - ), - ) - : null, - ), - ), - SizedBox(height: 16), - Text( - 'Halo,', - style: TextStyle( - color: Colors.white70, - fontSize: 16, - ), - ), - Text( - controller.nama, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 22, - ), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - 'Warga', - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - SizedBox(width: 8), - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.location_on, - color: Colors.white, - size: 14, - ), - SizedBox(width: 4), - Text( - controller.desa ?? 'Tidak ada desa', - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ], - ), + return Obx(() { + Map> menuCategories = { + 'Menu Utama': [ + DrawerMenuItem( + icon: Icons.dashboard_outlined, + activeIcon: Icons.dashboard, + title: 'Dashboard', + isSelected: controller.activeTabIndex.value == 0, + onTap: () { + controller.activeTabIndex.value = 0; + }, ), - Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: [ - _buildMenuCategory('Menu Utama'), - Obx(() => _buildMenuItem( - icon: Icons.dashboard_outlined, - activeIcon: Icons.dashboard, - title: 'Dashboard', - isSelected: controller.activeTabIndex.value == 0, - onTap: () { - Navigator.pop(context); - controller.changeTab(0); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.volunteer_activism_outlined, - activeIcon: Icons.volunteer_activism, - title: 'Penerimaan', - isSelected: controller.activeTabIndex.value == 1, - onTap: () { - Navigator.pop(context); - controller.changeTab(1); - }, - )), - Obx(() => _buildMenuItem( - icon: Icons.report_problem_outlined, - activeIcon: Icons.report_problem, - title: 'Pengaduan', - isSelected: controller.activeTabIndex.value == 2, - onTap: () { - Navigator.pop(context); - controller.changeTab(2); - }, - )), - _buildMenuCategory('Pengaturan'), - _buildMenuItem( - icon: Icons.person_outline, - activeIcon: Icons.person, - title: 'Profil', - onTap: () async { - Navigator.pop(context); - await Get.toNamed('/profile'); - // Refresh data ketika kembali dari profil - controller.refreshData(); - }, - ), - _buildMenuItem( - icon: Icons.info_outline, - activeIcon: Icons.info, - title: 'Tentang Kami', - onTap: () { - Navigator.pop(context); - Get.toNamed('/about'); - }, - ), - _buildMenuItem( - icon: Icons.logout, - title: 'Keluar', - onTap: () { - Navigator.pop(context); - controller.logout(); - }, - isLogout: true, - ), - ], - ), + DrawerMenuItem( + icon: Icons.volunteer_activism_outlined, + activeIcon: Icons.volunteer_activism, + title: 'Penerimaan Bantuan', + isSelected: controller.activeTabIndex.value == 1, + badgeCount: controller.totalPenyaluranDiterima.value > 0 + ? controller.totalPenyaluranDiterima.value + : null, + badgeColor: Colors.green, + onTap: () { + controller.activeTabIndex.value = 1; + }, ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - '© ${DateTime.now().year} DisalurKita', - style: TextStyle( - fontSize: 12, - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), + DrawerMenuItem( + icon: Icons.report_problem_outlined, + activeIcon: Icons.report_problem, + title: 'Pengaduan', + isSelected: controller.activeTabIndex.value == 2, + badgeCount: controller.totalPengaduanProses.value > 0 + ? controller.totalPengaduanProses.value + : null, + badgeColor: Colors.orange, + onTap: () { + controller.activeTabIndex.value = 2; + }, ), ], - ), - ); - } - - Widget _buildMenuCategory(String title) { - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), - child: Text( - title, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ); - } - - Widget _buildMenuItem({ - required IconData icon, - IconData? activeIcon, - required String title, - bool isSelected = false, - String? badge, - required Function() onTap, - bool isLogout = false, - }) { - return AnimatedContainer( - duration: Duration(milliseconds: 200), - decoration: BoxDecoration( - color: isSelected - ? AppTheme.primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - margin: EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: ListTile( - leading: Icon( - isSelected ? (activeIcon ?? icon) : icon, - color: isSelected - ? AppTheme.primaryColor - : (isLogout ? Colors.red : null), - ), - title: Text( - title, - style: TextStyle( - color: isSelected - ? AppTheme.primaryColor - : (isLogout ? Colors.red : null), - fontWeight: isSelected ? FontWeight.bold : null, + 'Pengaturan': [ + DrawerMenuItem( + icon: Icons.person_outline, + activeIcon: Icons.person, + title: 'Profil', + onTap: () { + Get.toNamed('/profile'); + }, ), - ), - trailing: badge != null - ? Container( - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(10), - ), - constraints: BoxConstraints( - minWidth: 20, - minHeight: 20, - ), - child: Text( - badge, - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ) - : null, - onTap: onTap, - ), - ); + DrawerMenuItem( + icon: Icons.info_outline, + activeIcon: Icons.info, + title: 'Tentang Kami', + onTap: () { + Get.toNamed('/about'); + }, + ), + DrawerMenuItem( + icon: Icons.logout, + title: 'Keluar', + isLogout: true, + onTap: () { + controller.logout(); + }, + ), + ], + }; + + return AppDrawer( + nama: controller.nama, + role: 'Warga', + desa: controller.desa, + avatar: controller.fotoProfil.value, + menuItems: const [], // Tidak digunakan karena menggunakan menuCategories + menuCategories: menuCategories, + onLogout: controller.logout, + footerText: '© ${DateTime.now().year} DisalurKita', + ); + }); } } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 32e76b7..0533e9a 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -42,6 +42,7 @@ abstract class Routes { static const riwayatPengaduan = _Paths.riwayatPengaduan; static const qrScanner = _Paths.qrScanner; static const konfirmasiPenerimaQr = _Paths.konfirmasiPenerimaQr; + static const jadwalPenyaluran = _Paths.jadwalPenyaluran; static const PENGADUAN = '/pengaduan'; static const TAMBAH_PENGADUAN = '/tambah-pengaduan'; static const PENGADUAN_DETAIL = '/pengaduan-detail'; @@ -91,6 +92,7 @@ abstract class _Paths { static const riwayatPengaduan = '/petugas-desa/riwayat-pengaduan'; static const qrScanner = '/petugas-desa/qr-scanner'; static const konfirmasiPenerimaQr = '/petugas-desa/konfirmasi-penerima/:id'; + static const jadwalPenyaluran = '/petugas-desa/jadwal-penyaluran'; static const PENGADUAN = '/pengaduan'; static const TAMBAH_PENGADUAN = '/tambah-pengaduan'; static const PENGADUAN_DETAIL = '/pengaduan-detail'; diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index f7f0bc8..15603da 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -2370,4 +2370,39 @@ class SupabaseService extends GetxService { return null; } } + + // Metode untuk mengupdate role user dengan SQL langsung + Future updateUserRole(String userId, int roleId) async { + try { + // Coba update via RPC jika sudah diimplementasikan + try { + await client.rpc( + 'update_user_role', + params: { + 'user_id': userId, + 'role_id_value': roleId, + }, + ); + print('DEBUG: Berhasil mengupdate role_id user via RPC'); + return; + } catch (e) { + print('WARN: RPC update_user_role tidak tersedia: $e'); + } + + // Jika RPC tidak tersedia, gunakan query SQL langsung ke auth.users jika diizinkan + try { + print('DEBUG: Mencoba update auth.users langsung'); + await client + .from('auth.users') + .update({'role_id': roleId}).eq('id', userId); + print('DEBUG: Berhasil mengupdate role_id user via SQL langsung'); + } catch (e) { + print('ERROR: Gagal mengupdate auth.users: $e'); + print('INFO: Role perlu diupdate manual melalui dashboard Supabase'); + } + } catch (e) { + print('ERROR: Gagal mengupdate role user: $e'); + // Tidak perlu throw exception, kegagalan update role tidak fatal + } + } } diff --git a/lib/app/widgets/app_drawer.dart b/lib/app/widgets/app_drawer.dart index 2b1c2f5..99f83ff 100644 --- a/lib/app/widgets/app_drawer.dart +++ b/lib/app/widgets/app_drawer.dart @@ -10,6 +10,9 @@ class AppDrawer extends StatelessWidget { final int? notificationCount; final VoidCallback onLogout; final List menuItems; + final Map>? menuCategories; + final String? footerText; + final Widget? headerExtraContent; const AppDrawer({ super.key, @@ -20,25 +23,101 @@ class AppDrawer extends StatelessWidget { this.notificationCount, required this.onLogout, required this.menuItems, + this.menuCategories, + this.footerText, + this.headerExtraContent, }); @override Widget build(BuildContext context) { return Drawer( - child: ListView( - padding: EdgeInsets.zero, + child: Column( children: [ - DrawerHeader( - decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, + _buildHeader(context), + if (menuCategories != null && menuCategories!.isNotEmpty) + _buildCategorizedMenu() + else + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ...menuItems.map((item) => _buildMenuItem(context, item)), + const Divider(), + ListTile( + leading: const Icon(Icons.person_outline), + title: const Text('Profil'), + onTap: () { + Navigator.pop(context); + Get.toNamed('/profile'); + }, + ), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('Tentang Kami'), + onTap: () { + Navigator.pop(context); + Get.toNamed('/about'); + }, + ), + ListTile( + leading: const Icon(Icons.logout), + title: const Text('Keluar'), + onTap: () { + Navigator.pop(context); + onLogout(); + }, + ), + ], + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - CircleAvatar( - radius: 30, - backgroundColor: AppTheme.primaryColor.withOpacity(0.2), + if (footerText != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + footerText!, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return 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: [ + Row( + 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: avatar != null && avatar!.isNotEmpty ? NetworkImage(avatar!) : null, @@ -47,122 +126,249 @@ class AppDrawer extends StatelessWidget { nama.isNotEmpty ? nama.substring(0, 1).toUpperCase() : '?', - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + color: Colors.blue.shade700, fontSize: 24, ), ) : null, ), - const SizedBox(height: 10), - Text( - nama, + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'DisalurKita', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 3), + const Text( + 'Salurkan dengan Pasti, Pantau dengan Bukti', + style: TextStyle( + fontSize: 10, + color: Colors.white70, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Halo,', + style: TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + Text( + 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: Text( + role, style: const TextStyle( color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + fontSize: 12, ), ), - Text( - desa != null ? '$role - $desa' : role, - style: TextStyle( - color: Colors.white.withOpacity(0.8), - fontSize: 14, + ), + if (desa != null) ...[ + 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( + desa!, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], ), ), ], - ), - ), - ...menuItems.map((item) => _buildMenuItem(context, item)), - const Divider(), - ListTile( - leading: const Icon(Icons.person_outline), - title: const Text('Profil'), - onTap: () { - Navigator.pop(context); - Get.toNamed('/profile'); - }, - ), - ListTile( - leading: const Icon(Icons.settings_outlined), - title: const Text('Pengaturan'), - onTap: () { - Navigator.pop(context); - // TODO: Navigasi ke halaman pengaturan - }, - ), - ListTile( - leading: const Icon(Icons.logout), - title: const Text('Keluar'), - onTap: () { - Navigator.pop(context); - onLogout(); - }, + ], ), + if (headerExtraContent != null) ...[ + const SizedBox(height: 8), + headerExtraContent!, + ], ], ), ); } - Widget _buildMenuItem(BuildContext context, DrawerMenuItem item) { - return ListTile( - leading: item.badgeCount != null && item.badgeCount! > 0 - ? Stack( - alignment: Alignment.center, - children: [ - Icon(item.icon), - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: item.badgeColor ?? Colors.red, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 12, - minHeight: 12, - ), - child: Text( - item.badgeCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 8, + Widget _buildCategorizedMenu() { + return Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ...menuCategories!.entries.map((entry) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMenuCategory(entry.key), + ...entry.value.map((item) => _buildMenuItem(null, item)), + ], + )), + ], + ), + ); + } + + Widget _buildMenuCategory(String title) { + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), + child: Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + ); + } + + Widget _buildMenuItem(BuildContext? context, DrawerMenuItem item) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: item.isSelected + ? AppTheme.primaryColor.withOpacity(0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: ListTile( + leading: item.badgeCount != null && item.badgeCount! > 0 + ? Stack( + alignment: Alignment.center, + children: [ + Icon( + item.isSelected + ? (item.activeIcon ?? item.icon) + : item.icon, + color: item.isSelected + ? AppTheme.primaryColor + : (item.isLogout ? Colors.red : null), + ), + Positioned( + top: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: item.badgeColor ?? Colors.red, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints( + minWidth: 12, + minHeight: 12, + ), + child: Text( + item.badgeCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 8, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), - ), - ], - ) - : Icon(item.icon), - title: Text(item.title), - selected: item.isSelected, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - item.onTap(); - }, + ], + ) + : Icon( + item.isSelected ? (item.activeIcon ?? item.icon) : item.icon, + color: item.isSelected + ? AppTheme.primaryColor + : (item.isLogout ? Colors.red : null), + ), + title: Text( + item.title, + style: TextStyle( + color: item.isSelected + ? AppTheme.primaryColor + : (item.isLogout ? Colors.red : null), + fontWeight: item.isSelected ? FontWeight.bold : null, + ), + ), + trailing: item.trailing, + onTap: () { + if (context != null) { + Navigator.pop(context); + } else { + Navigator.of(Get.overlayContext!).pop(); + } + + Future.delayed(const Duration(milliseconds: 100), () { + item.onTap(); + }); + }, + ), ); } } class DrawerMenuItem { final IconData icon; + final IconData? activeIcon; final String title; final bool isSelected; final VoidCallback onTap; final int? badgeCount; final Color? badgeColor; + final bool isLogout; + final Widget? trailing; DrawerMenuItem({ required this.icon, + this.activeIcon, required this.title, this.isSelected = false, required this.onTap, this.badgeCount, this.badgeColor, + this.isLogout = false, + this.trailing, }); } diff --git a/lib/app/widgets/bantuan_card.dart b/lib/app/widgets/bantuan_card.dart index e074d33..fa0a657 100644 --- a/lib/app/widgets/bantuan_card.dart +++ b/lib/app/widgets/bantuan_card.dart @@ -68,7 +68,7 @@ class BantuanCard extends StatelessWidget { ), child: Icon( item.isUang == true - ? Icons.attach_money + ? Icons.payment_rounded : Icons.inventory_2, color: item.isUang == true ? Colors.green : Colors.blue, size: 28, @@ -81,15 +81,36 @@ class BantuanCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - item.kategoriNama ?? 'Bantuan', - style: TextStyle( - color: Colors.grey.shade700, - fontSize: 14, - fontWeight: FontWeight.w500, + if (item.namaPenyaluran != null && + item.namaPenyaluran!.isNotEmpty) + Text( + item.namaPenyaluran!, + style: TextStyle( + color: (item.isUang == true) + ? Colors.green.shade800 + : Colors.blue.shade800, + fontSize: 15, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Padding( + padding: EdgeInsets.only( + top: item.namaPenyaluran != null && + item.namaPenyaluran!.isNotEmpty + ? 4 + : 0), + child: Text( + item.kategoriNama ?? 'Bantuan', + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, ), const SizedBox(height: 6), Row( @@ -229,12 +250,26 @@ class BantuanCard extends StatelessWidget { children: [ Icon( item.isUang == true - ? Icons.attach_money + ? Icons.payment_rounded : Icons.inventory_2, color: item.isUang == true ? Colors.green.shade700 : Colors.blue.shade700, ), + const SizedBox(width: 12), + Expanded( + child: Text( + item.namaPenyaluran != null && + item.namaPenyaluran!.isNotEmpty + ? item.namaPenyaluran! + : "Bantuan ${item.kategoriNama ?? ''}", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index dc1c0b3..4d6a7a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -241,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" + url: "https://pub.dev" + source: hosted + version: "0.70.2" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index b245574..0573985 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,6 +96,7 @@ dependencies: cached_network_image: ^3.3.1 share_plus: ^10.1.4 path: ^1.9.1 + fl_chart: ^0.70.2 dev_dependencies: flutter_test: