From 32736be867a5487383e977f04bb99d64c370819a Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Tue, 25 Mar 2025 12:21:37 +0700 Subject: [PATCH] Perbarui model dan tampilan untuk menambahkan properti fotoProfil di DonaturModel, PetugasDesaModel, dan WargaModel. Modifikasi controller dan tampilan untuk mendukung pengambilan dan penampilan foto profil pengguna. Tambahkan fungsionalitas baru untuk menampilkan foto profil di berbagai tampilan, termasuk detail penerima dan dashboard warga. Perbarui rute aplikasi untuk mencakup halaman profil pengguna. --- .../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 +- lib/app/data/models/donatur_model.dart | 4 + lib/app/data/models/petugas_desa_model.dart | 4 + lib/app/data/models/warga_model.dart | 4 + .../controllers/penerima_controller.dart | 103 +++- .../controllers/petugas_desa_controller.dart | 50 ++ .../views/daftar_penerima_view.dart | 325 ++++++----- .../views/detail_donatur_view.dart | 504 ++++++++++-------- .../views/detail_penerima_view.dart | 402 ++++++++++++-- .../petugas_desa/views/petugas_desa_view.dart | 468 +++++++++++----- .../controllers/profile_controller.dart | 112 +++- .../modules/profile/views/profile_view.dart | 262 ++++++++- .../warga_dashboard_controller.dart | 79 +++ .../warga/views/warga_dashboard_view.dart | 337 ++++++++---- lib/app/routes/app_routes.dart | 10 + lib/app/services/supabase_service.dart | 130 +++-- 19 files changed, 2138 insertions(+), 752 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 b8cb640..626e68c 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 093da6a..407cbe3 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 bc005df..97e038e 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 4d1087f..d63f9af 100644 --- a/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/x86_64/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  2 2~ +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  2 2~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  2  2{ +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  2  2{ y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  2 2 +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  2 2 ~ -|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  2 2m +|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  2 2m k -iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  2 2q +iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  2 2q o -mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  2v +mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  2v t -rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  2 K 2w +rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  2 K 2w u -sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  2 { +sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  2 { y -wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  2 +wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  2   -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  2  2t +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  2  2t r -pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  2  ( 2y +pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  2  ( 2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\symbol_folder_index.txt  2  l 2 \ No newline at end of file +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\symbol_folder_index.txt  2  l 2 \ No newline at end of file diff --git a/lib/app/data/models/donatur_model.dart b/lib/app/data/models/donatur_model.dart index 939b5f0..21d94da 100644 --- a/lib/app/data/models/donatur_model.dart +++ b/lib/app/data/models/donatur_model.dart @@ -9,6 +9,7 @@ class DonaturModel { final String? jenis; final String? deskripsi; final String? status; + final String? fotoProfil; final DateTime? createdAt; final DateTime? updatedAt; @@ -21,6 +22,7 @@ class DonaturModel { this.jenis, this.deskripsi, this.status = 'AKTIF', + this.fotoProfil, this.createdAt, this.updatedAt, }); @@ -39,6 +41,7 @@ class DonaturModel { jenis: json["jenis"], deskripsi: json["deskripsi"], status: json["status"] ?? 'AKTIF', + fotoProfil: json["foto_profil"], createdAt: json["created_at"] != null ? DateTime.parse(json["created_at"]) : null, @@ -56,6 +59,7 @@ class DonaturModel { "jenis": jenis, "deskripsi": deskripsi, "status": status ?? 'AKTIF', + "foto_profil": fotoProfil, "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; diff --git a/lib/app/data/models/petugas_desa_model.dart b/lib/app/data/models/petugas_desa_model.dart index 9a8fd10..1fc88b9 100644 --- a/lib/app/data/models/petugas_desa_model.dart +++ b/lib/app/data/models/petugas_desa_model.dart @@ -10,6 +10,7 @@ class PetugasDesaModel { final String? email; final String? jabatan; final String? nip; + final String? fotoProfil; final DateTime? createdAt; final DateTime? updatedAt; final DesaModel? desa; @@ -23,6 +24,7 @@ class PetugasDesaModel { this.email, this.jabatan, this.nip, + this.fotoProfil, this.createdAt, this.updatedAt, this.desa, @@ -48,6 +50,7 @@ class PetugasDesaModel { email: json["email"], jabatan: json["jabatan"], nip: json["nip"], + fotoProfil: json["foto_profil"], createdAt: json["created_at"] != null ? DateTime.parse(json["created_at"]) : null, @@ -67,6 +70,7 @@ class PetugasDesaModel { "email": email, "jabatan": jabatan, "nip": nip, + "foto_profil": fotoProfil, "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; diff --git a/lib/app/data/models/warga_model.dart b/lib/app/data/models/warga_model.dart index 9a4b00a..2876e17 100644 --- a/lib/app/data/models/warga_model.dart +++ b/lib/app/data/models/warga_model.dart @@ -17,6 +17,7 @@ class WargaModel { final String? kategoriEkonomi; final String? status; final String? catatan; + final String? fotoProfil; final DateTime? createdAt; final DateTime? updatedAt; final DesaModel? desa; @@ -36,6 +37,7 @@ class WargaModel { this.kategoriEkonomi, this.status = 'AKTIF', this.catatan, + this.fotoProfil, this.createdAt, this.updatedAt, this.desa, @@ -69,6 +71,7 @@ class WargaModel { kategoriEkonomi: json["kategori_ekonomi"], status: json["status"] ?? 'AKTIF', catatan: json["catatan"], + fotoProfil: json["foto_profil"], createdAt: json["created_at"] != null ? DateTime.parse(json["created_at"]) : null, @@ -94,6 +97,7 @@ class WargaModel { "kategori_ekonomi": kategoriEkonomi, "status": status, "catatan": catatan, + "foto_profil": fotoProfil, "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; diff --git a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart index cbfeb8a..02aadab 100644 --- a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/utils/date_time_helper.dart'; class PenerimaController extends GetxController { @@ -7,6 +8,11 @@ class PenerimaController extends GetxController { >[].obs; final RxBool isLoading = false.obs; + // Variabel untuk menyimpan daftar penyaluran bantuan untuk penerima tertentu + final RxList> daftarPenyaluran = + >[].obs; + final RxBool isLoadingPenyaluran = false.obs; + // Variabel untuk halaman konfirmasi penerima final RxBool isKonfirmasiChecked = false.obs; final RxBool isIdentitasChecked = false.obs; @@ -37,9 +43,30 @@ class PenerimaController extends GetxController { super.onClose(); } - void fetchDaftarPenerima() { + void fetchDaftarPenerima() async { isLoading.value = true; + try { + // Get data penerima dari database + final penerimaBantuan = await SupabaseService.to.getPenerimaBantuan(); + + if (penerimaBantuan != null) { + daftarPenerima.value = penerimaBantuan; + } else { + // Gunakan data dummy jika gagal mendapatkan data dari database + _loadDummyData(); + } + } catch (e) { + print('Error fetching penerima: $e'); + // Gunakan data dummy sebagai fallback + _loadDummyData(); + } finally { + isLoading.value = false; + } + } + + // Metode untuk memuat data dummy + void _loadDummyData() { // Simulasi data penerima Future.delayed(const Duration(milliseconds: 500), () { daftarPenerima.value = [ @@ -134,7 +161,6 @@ class PenerimaController extends GetxController { 'terverifikasi': true, }, ]; - isLoading.value = false; }); } @@ -167,6 +193,79 @@ class PenerimaController extends GetxController { } } + // Metode untuk mengambil daftar penyaluran bantuan berdasarkan ID warga + Future fetchPenyaluranByWargaId(String wargaId) async { + isLoadingPenyaluran.value = true; + daftarPenyaluran.clear(); + + try { + final penyaluranBantuan = + await SupabaseService.to.getPenyaluranBantuanByWargaId(wargaId); + + if (penyaluranBantuan != null && penyaluranBantuan.isNotEmpty) { + daftarPenyaluran.value = penyaluranBantuan; + } else { + // Gunakan data dummy jika tidak ada data dari database + _loadDummyPenyaluran(wargaId); + } + } catch (e) { + print('Error fetching penyaluran bantuan: $e'); + // Gunakan data dummy sebagai fallback + _loadDummyPenyaluran(wargaId); + } finally { + isLoadingPenyaluran.value = false; + } + } + + // Metode untuk memuat data dummy penyaluran + void _loadDummyPenyaluran(String wargaId) { + // Data dummy penyaluran bantuan (hanya untuk demo) + daftarPenyaluran.value = [ + { + 'id': '1', + 'penerima_id': wargaId, + 'tanggal_penyaluran': + DateTime.now().subtract(const Duration(days: 5)).toIso8601String(), + 'status': 'TERLAKSANA', + 'stok_bantuan': { + 'nama': 'Paket Sembako', + 'jenis': 'Bahan Pokok', + 'kuantitas': '1 Paket', + }, + 'keterangan': 'Bantuan pangan rutin bulanan', + 'bukti_penyaluran': 'assets/images/bukti_penyaluran.jpg', + }, + { + 'id': '2', + 'penerima_id': wargaId, + 'tanggal_penyaluran': + DateTime.now().subtract(const Duration(days: 35)).toIso8601String(), + 'status': 'TERLAKSANA', + 'stok_bantuan': { + 'nama': 'Bantuan Pendidikan', + 'jenis': 'Alat Tulis', + 'kuantitas': '1 Paket', + }, + 'keterangan': 'Bantuan sekolah semester baru', + 'bukti_penyaluran': 'assets/images/bukti_penyaluran.jpg', + }, + { + 'id': '3', + 'penerima_id': wargaId, + 'tanggal_penyaluran': + DateTime.now().add(const Duration(days: 2)).toIso8601String(), + 'status': 'DIJADWALKAN', + 'stok_bantuan': { + 'nama': 'Paket Sembako', + 'jenis': 'Bahan Pokok', + 'kuantitas': '1 Paket', + }, + 'keterangan': 'Bantuan pangan rutin bulanan', + 'bukti_penyaluran': null, + }, + ]; + } + // Fungsi untuk memilih tanggal penyaluran Future pilihTanggalPenyaluran(BuildContext context) async { final DateTime? picked = await showDatePicker( diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart index eb31efb..813638a 100644 --- a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart @@ -27,6 +27,9 @@ class PetugasDesaController extends GetxController { // Data profil pengguna dari cache final RxMap userProfile = RxMap({}); + // Variabel untuk foto profil + final RxString fotoProfil = ''.obs; + // Model desa dari cache final Rx desaModel = Rx(null); @@ -102,6 +105,33 @@ class PetugasDesaController extends GetxController { return 'Petugas Desa'; } + // Getter untuk foto profil + String? get profilePhotoUrl { + // 1. Coba ambil dari fotoProfil yang sudah disimpan + if (fotoProfil.isNotEmpty) { + return fotoProfil.value; + } + + // 2. Coba ambil dari roleData jika merupakan PetugasDesaModel + final userData = _authController.userData; + if (userData != null && userData.roleData is PetugasDesaModel) { + final petugasData = userData.roleData as PetugasDesaModel; + if (petugasData.fotoProfil != null && + petugasData.fotoProfil!.isNotEmpty) { + return petugasData.fotoProfil; + } + } + + // 3. Coba ambil dari role_data di userProfile + if (userProfile['role_data'] != null && + userProfile['role_data'] is Map && + userProfile['role_data']['foto_profil'] != null) { + return userProfile['role_data']['foto_profil']; + } + + return null; + } + // Getter untuk counter dari CounterService RxInt get jumlahNotifikasiBelumDibaca => _counterService.jumlahNotifikasiBelumDibaca; @@ -212,6 +242,14 @@ class PetugasDesaController extends GetxController { 'desa': petugasData.desa?.toJson(), }; + // Ambil foto profil jika ada + if (petugasData.fotoProfil != null && + petugasData.fotoProfil!.isNotEmpty) { + fotoProfil.value = petugasData.fotoProfil!; + print( + 'DEBUG: Foto profil dari petugasData: ${fotoProfil.value}'); + } + return; // Data sudah lengkap, tidak perlu fetch lagi } } @@ -223,6 +261,14 @@ class PetugasDesaController extends GetxController { if (baseProfile != null) { userProfile.value = baseProfile; + // Cek dan ambil foto profil + if (baseProfile['role_data'] != null && + baseProfile['role_data'] is Map && + baseProfile['role_data']['foto_profil'] != null) { + fotoProfil.value = baseProfile['role_data']['foto_profil']; + print('DEBUG: Foto profil dari API: ${fotoProfil.value}'); + } + if (baseProfile['desa'] != null && baseProfile['desa'] is Map) { try { @@ -594,7 +640,9 @@ class PetugasDesaController extends GetxController { // Metode untuk mengubah tab aktif void changeTab(int index) { + print('Mengubah tab ke index: $index (dari: ${activeTabIndex.value})'); activeTabIndex.value = index; + print('activeTabIndex sekarang: ${activeTabIndex.value}'); // Jika tab penitipan dipilih, muat ulang data penitipan if (index == 2) { @@ -621,6 +669,8 @@ class PetugasDesaController extends GetxController { print('Error saat memanggil onTabReactivated: $e'); } } + // Paksa update UI + activeTabIndex.refresh(); } // Metode untuk logout diff --git a/lib/app/modules/petugas_desa/views/daftar_penerima_view.dart b/lib/app/modules/petugas_desa/views/daftar_penerima_view.dart index a3256f4..b76e730 100644 --- a/lib/app/modules/petugas_desa/views/daftar_penerima_view.dart +++ b/lib/app/modules/petugas_desa/views/daftar_penerima_view.dart @@ -161,173 +161,205 @@ class DaftarPenerimaView extends GetView { Widget _buildPenerimaCard( BuildContext context, Map penerima) { - return Card( + final statusActive = penerima['status'] == 'AKTIF'; + + return Container( margin: const EdgeInsets.only(bottom: 16), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), - child: InkWell( - onTap: () { - // Navigasi ke halaman detail penerima - Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Foto profil - CircleAvatar( - radius: 30, - backgroundColor: AppTheme.primaryColor.withOpacity(0.1), - child: penerima['foto'] != null - ? ClipRRect( - borderRadius: BorderRadius.circular(30), - child: Image.asset( - penerima['foto'], - width: 60, - height: 60, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 30, - color: AppTheme.primaryColor, - ); - }, - ), - ) - : const Icon( - Icons.person, - size: 30, - color: AppTheme.primaryColor, - ), + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + onTap: () { + // Navigasi ke halaman detail penerima + Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']); + }, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + AppTheme.primaryColor.withOpacity(0.03), + ], ), - const SizedBox(width: 16), - // Informasi penerima - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - penerima['nama'] ?? '', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + ), + child: Row( + children: [ + // Foto profil dengan animasi hero + Hero( + tag: 'penerima-${penerima['id']}', + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.3), + width: 2), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: CircleAvatar( + radius: 35, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + backgroundImage: penerima['foto_profil'] != null + ? NetworkImage(penerima['foto_profil']) + : null, + child: penerima['foto_profil'] == null + ? Icon( + Icons.person, + size: 35, + color: AppTheme.primaryColor.withOpacity(0.7), + ) + : null, + ), + ), + ), + const SizedBox(width: 16), + // Informasi penerima + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + penerima['nama_lengkap'] ?? 'Tanpa Nama', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ), + if (penerima['terverifikasi'] == true) + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.verified, + color: Colors.green, + size: 18, + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade300, + width: 1, ), ), - if (penerima['terverifikasi'] == true) + child: Text( + 'NIK: ${penerima['nik'] ?? 'Belum Ada'}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ Container( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), + horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: statusActive + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: statusActive + ? Colors.green.withOpacity(0.3) + : Colors.red.withOpacity(0.3), + width: 1, + ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.verified, - size: 14, - color: Colors.green, + Icon( + statusActive + ? Icons.check_circle + : Icons.cancel, + size: 12, + color: + statusActive ? Colors.green : Colors.red, ), const SizedBox(width: 4), - const Text( - 'Terverifikasi', + Text( + statusActive ? 'Aktif' : 'Tidak Aktif', style: TextStyle( fontSize: 12, - color: Colors.green, + fontWeight: FontWeight.w500, + color: statusActive + ? Colors.green + : Colors.red, ), ), ], ), ), - ], - ), - const SizedBox(height: 4), - Text( - 'NIK: ${penerima['nik'] ?? ''}', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], + const Spacer(), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: AppTheme.primaryColor, + ), + ), + ], ), - ), - const SizedBox(height: 4), - - _buildStatusBadge(penerima['status']), - // const SizedBox(height: 8), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // penerima['alamatLengkap'] ?? '', - // style: TextStyle( - // fontSize: 12, - // color: Colors.grey[600], - // ), - // maxLines: 1, - // overflow: TextOverflow.ellipsis, - // ), - // _buildStatusBadge(penerima['status']), - // ], - // ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ); } - - Widget _buildStatusBadge(String? status) { - Color backgroundColor; - Color textColor; - - switch (status) { - case 'Selesai': - backgroundColor = Colors.green.withOpacity(0.1); - textColor = Colors.green; - break; - case 'Terjadwal': - backgroundColor = Colors.blue.withOpacity(0.1); - textColor = Colors.blue; - break; - case 'Belum disalurkan': - backgroundColor = Colors.orange.withOpacity(0.1); - textColor = Colors.orange; - break; - default: - backgroundColor = Colors.grey.withOpacity(0.1); - textColor = Colors.grey; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - status ?? 'Tidak diketahui', - style: TextStyle( - fontSize: 12, - color: textColor, - ), - ), - ); - } } class PenerimaSearchDelegate extends SearchDelegate { @@ -369,7 +401,7 @@ class PenerimaSearchDelegate extends SearchDelegate { Widget _buildSearchResults() { final filteredList = daftarPenerima.where((penerima) { - final nama = penerima['nama']?.toString().toLowerCase() ?? ''; + final nama = penerima['nama_lengkap']?.toString().toLowerCase() ?? ''; final nik = penerima['nik']?.toString().toLowerCase() ?? ''; final alamat = penerima['alamatLengkap']?.toString().toLowerCase() ?? ''; final searchLower = query.toLowerCase(); @@ -403,13 +435,18 @@ class PenerimaSearchDelegate extends SearchDelegate { }, leading: CircleAvatar( backgroundColor: AppTheme.primaryColor.withOpacity(0.1), - child: const Icon( - Icons.person, - color: AppTheme.primaryColor, - ), + backgroundImage: penerima['foto_profil'] != null + ? NetworkImage(penerima['foto_profil']) + : null, + child: penerima['foto_profil'] == null + ? const Icon( + Icons.person, + color: AppTheme.primaryColor, + ) + : null, ), title: Text( - penerima['nama'] ?? '', + penerima['nama_lengkap'] ?? '', style: const TextStyle( fontWeight: FontWeight.bold, ), 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 3c489fc..84e01b4 100644 --- a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart @@ -79,12 +79,263 @@ class DetailDonaturView extends GetView { final totalNilaiDonasiUangFormatted = controller.formatRupiah(totalNilaiDonasiUang); + // Pilih warna berdasarkan jenis donatur + Color jenisColor = donatur.jenis == 'Perusahaan' + ? Colors.blue + : donatur.jenis == 'Organisasi' + ? Colors.green + : Colors.orange; + return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header dengan informasi utama donatur + // Header dengan informasi utama donatur - desain yang lebih menarik + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: jenisColor.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Card( + elevation: 0, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + jenisColor.withOpacity(0.05), + ], + ), + ), + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Avatar dan nama donatur dengan layout yang lebih baik + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: jenisColor.withOpacity(0.7), width: 2), + boxShadow: [ + BoxShadow( + color: jenisColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Hero( + tag: 'donatur-${donatur.id}', + child: CircleAvatar( + radius: 45, + backgroundColor: jenisColor.withOpacity(0.1), + backgroundImage: donatur.fotoProfil != null && + donatur.fotoProfil!.isNotEmpty + ? NetworkImage(donatur.fotoProfil!) + : null, + child: (donatur.fotoProfil == null || + donatur.fotoProfil!.isEmpty) + ? Icon( + jenisIcon, + size: 45, + color: jenisColor.withOpacity(0.8), + ) + : null, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + donatur.nama ?? 'Tanpa Nama', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: jenisColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: jenisColor.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + jenisIcon, + size: 16, + color: jenisColor, + ), + const SizedBox(width: 6), + Text( + donatur.jenis ?? 'Tidak Diketahui', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: jenisColor, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: donatur.status == 'AKTIF' + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: donatur.status == 'AKTIF' + ? Colors.green.withOpacity(0.3) + : Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + donatur.status == 'AKTIF' + ? Icons.check_circle + : Icons.cancel, + size: 16, + color: donatur.status == 'AKTIF' + ? Colors.green + : Colors.red, + ), + const SizedBox(width: 6), + Text( + donatur.status == 'AKTIF' + ? 'Aktif' + : 'Tidak Aktif', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: donatur.status == 'AKTIF' + ? Colors.green + : Colors.red, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 24), + + // Summary cards for donations + Row( + children: [ + Expanded( + child: _buildSummaryCard( + title: 'Total Donasi', + value: jumlahDonasi.toString(), + icon: Icons.volunteer_activism, + color: jenisColor, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildSummaryCard( + title: 'Donasi Uang', + value: jumlahDonasiUang.toString(), + icon: Icons.attach_money, + color: jenisColor, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildSummaryCard( + title: 'Donasi Barang', + value: jumlahDonasiBarang.toString(), + icon: Icons.inventory_2, + color: jenisColor, + ), + ), + ], + ), + const SizedBox(height: 16), + // Total nilai donasi + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: jenisColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: jenisColor.withOpacity(0.2), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Total Nilai Donasi Uang', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 4), + Text( + totalNilaiDonasiUangFormatted, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: jenisColor, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 16), + // Informasi kontak Card( elevation: 2, shape: RoundedRectangleBorder( @@ -94,104 +345,6 @@ class DetailDonaturView extends GetView { padding: const EdgeInsets.all(16), child: Column( children: [ - // Avatar dan nama donatur - Row( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: AppTheme.primaryColor.withOpacity(0.1), - child: Icon( - jenisIcon, - size: 40, - color: AppTheme.primaryColor, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - donatur.nama ?? 'Tanpa Nama', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: donatur.jenis == 'Perusahaan' - ? Colors.blue.withOpacity(0.1) - : donatur.jenis == 'Organisasi' - ? Colors.green.withOpacity(0.1) - : Colors.orange.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - donatur.jenis ?? 'Tidak Diketahui', - style: TextStyle( - fontSize: 12, - color: donatur.jenis == 'Perusahaan' - ? Colors.blue - : donatur.jenis == 'Organisasi' - ? Colors.green - : Colors.orange, - ), - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: donatur.status == 'AKTIF' - ? Colors.green.withOpacity(0.1) - : Colors.red.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - donatur.status == 'AKTIF' - ? Icons.check_circle - : Icons.cancel, - size: 12, - color: donatur.status == 'AKTIF' - ? Colors.green - : Colors.red, - ), - const SizedBox(width: 4), - Text( - donatur.status ?? 'TIDAK AKTIF', - style: TextStyle( - fontSize: 12, - color: donatur.status == 'AKTIF' - ? Colors.green - : Colors.red, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - // Informasi kontak const Divider(), const SizedBox(height: 8), _buildInfoItem(Icons.location_on, 'Alamat', @@ -215,85 +368,6 @@ class DetailDonaturView extends GetView { ), ), const SizedBox(height: 16), - // Ringkasan donasi - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Ringkasan Donasi', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildStatItem( - 'Total Donasi', - '$jumlahDonasi', - Icons.volunteer_activism, - Colors.blue, - ), - ), - Expanded( - child: _buildStatItem( - 'Donasi Uang', - '$jumlahDonasiUang', - Icons.monetization_on, - Colors.green, - ), - ), - Expanded( - child: _buildStatItem( - 'Donasi Barang', - '$jumlahDonasiBarang', - Icons.inventory_2, - Colors.orange, - ), - ), - ], - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - Row( - children: [ - const Icon( - Icons.monetization_on, - color: Colors.green, - ), - const SizedBox(width: 8), - const Text( - 'Total Nilai Donasi Uang:', - style: TextStyle( - fontSize: 16, - ), - ), - const Spacer(), - Text( - totalNilaiDonasiUangFormatted, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 16), // Riwayat donasi Card( elevation: 2, @@ -315,12 +389,6 @@ class DetailDonaturView extends GetView { fontWeight: FontWeight.bold, ), ), - TextButton( - onPressed: () { - // Navigasi ke halaman riwayat donasi lengkap - }, - child: const Text('Lihat Semua'), - ), ], ), const SizedBox(height: 8), @@ -368,37 +436,51 @@ class DetailDonaturView extends GetView { ); } - Widget _buildStatItem( - String label, String value, IconData icon, Color color) { - return Column( - children: [ - CircleAvatar( - radius: 25, - backgroundColor: color.withOpacity(0.1), - child: Icon( + Widget _buildSummaryCard({ + required String title, + required String value, + required IconData icon, + required Color color, + }) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: color.withOpacity(0.2), + width: 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( icon, - color: color, + size: 24, + color: color.withOpacity(0.7), ), - ), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: color, + const SizedBox(height: 8), + Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), ), - textAlign: TextAlign.center, - ), - ], + ], + ), ); } diff --git a/lib/app/modules/petugas_desa/views/detail_penerima_view.dart b/lib/app/modules/petugas_desa/views/detail_penerima_view.dart index ba55d19..b0fbf38 100644 --- a/lib/app/modules/petugas_desa/views/detail_penerima_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_penerima_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; +import 'package:intl/intl.dart'; class DetailPenerimaView extends GetView { const DetailPenerimaView({super.key}); @@ -10,6 +11,11 @@ class DetailPenerimaView extends GetView { Widget build(BuildContext context) { final String id = Get.arguments as String; + // Panggil metode untuk mengambil data penyaluran saat halaman dibuat + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchPenyaluranByWargaId(id); + }); + return Obx(() { if (controller.isLoading.value) { return Scaffold( @@ -51,6 +57,9 @@ class DetailPenerimaView extends GetView { // Status penyaluran _buildStatusSection(penerima), + // Riwayat Penyaluran Bantuan + _buildRiwayatPenyaluran(), + const SizedBox(height: 20), ], ), @@ -63,65 +72,100 @@ class DetailPenerimaView extends GetView { Widget _buildHeader(Map penerima) { return Container( width: double.infinity, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only(top: 24, bottom: 30, left: 16, right: 16), decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.primaryColor.withOpacity(0.8), + AppTheme.primaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), child: Column( children: [ - // Foto profil - CircleAvatar( - radius: 50, - backgroundColor: Colors.white, - child: penerima['foto'] != null - ? ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Image.asset( - penerima['foto'], - width: 100, - height: 100, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 50, - color: AppTheme.primaryColor, - ); - }, - ), - ) - : const Icon( - Icons.person, - size: 50, - color: AppTheme.primaryColor, - ), + // Foto profil dengan efek bayangan dan border + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 3), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Hero( + tag: 'penerima-${penerima['id']}', + child: CircleAvatar( + radius: 60, + backgroundColor: Colors.white, + backgroundImage: penerima['foto_profil'] != null + ? NetworkImage(penerima['foto_profil']) + : null, + child: penerima['foto_profil'] == null + ? Icon( + Icons.person, + size: 60, + color: AppTheme.primaryColor.withOpacity(0.7), + ) + : null, + ), + ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), - // Nama penerima + // Nama penerima dengan stroke effect Text( penerima['nama'] ?? '', style: const TextStyle( - fontSize: 24, + fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, + shadows: [ + Shadow( + blurRadius: 5.0, + color: Colors.black26, + offset: Offset(0, 2), + ), + ], ), + textAlign: TextAlign.center, ), const SizedBox(height: 8), - // NIK - Text( - penerima['nik'] ?? '', - style: TextStyle( - fontSize: 16, - color: Colors.white.withOpacity(0.8), + // NIK dengan style yang lebih menarik + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'NIK: ${penerima['nik'] ?? 'Belum terdaftar'}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.9), + ), ), ), const SizedBox(height: 16), - // Badge terverifikasi + // Badge terverifikasi dengan animasi if (penerima['terverifikasi'] == true) - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, @@ -129,13 +173,20 @@ class DetailPenerimaView extends GetView { decoration: BoxDecoration( color: AppTheme.successColor, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.successColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.verified, - size: 16, + size: 18, color: Colors.white, ), SizedBox(width: 8), @@ -143,12 +194,35 @@ class DetailPenerimaView extends GetView { 'Terverifikasi', style: TextStyle( fontSize: 14, + fontWeight: FontWeight.bold, color: Colors.white, ), ), ], ), ), + + // Informasi status aktif + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: penerima['status'] == 'AKTIF' + ? Colors.green.withOpacity(0.2) + : Colors.red.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + penerima['status'] == 'AKTIF' ? 'Aktif' : 'Tidak Aktif', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: penerima['status'] == 'AKTIF' + ? Colors.white + : Colors.white.withOpacity(0.9), + ), + ), + ), ], ), ); @@ -345,4 +419,252 @@ class DetailPenerimaView extends GetView { ), ); } + + // Widget untuk menampilkan riwayat penyaluran bantuan + Widget _buildRiwayatPenyaluran() { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Riwayat Penyaluran Bantuan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Obx(() { + if (controller.isLoadingPenyaluran.value) { + return const Center( + child: Padding( + padding: EdgeInsets.all(24.0), + child: CircularProgressIndicator(), + ), + ); + } + + if (controller.daftarPenyaluran.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Icon( + Icons.history_rounded, + size: 48, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'Belum ada riwayat penyaluran bantuan', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + ], + ), + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.daftarPenyaluran.length, + itemBuilder: (context, index) { + final penyaluran = controller.daftarPenyaluran[index]; + return _buildPenyaluranItem(penyaluran); + }, + ); + }), + ], + ), + ); + } + + // Widget untuk menampilkan item penyaluran bantuan + Widget _buildPenyaluranItem(Map penyaluran) { + final DateTime tanggalPenyaluran = + DateTime.parse(penyaluran['tanggal_penyaluran']); + final String formattedDate = + DateFormat('dd MMMM yyyy', 'id_ID').format(tanggalPenyaluran); + + final Color statusColor = penyaluran['status'] == 'TERLAKSANA' + ? AppTheme.completedColor + : penyaluran['status'] == 'DIJADWALKAN' + ? AppTheme.processedColor + : AppTheme.warningColor; + + final IconData statusIcon = penyaluran['status'] == 'TERLAKSANA' + ? Icons.check_circle + : penyaluran['status'] == 'DIJADWALKAN' + ? Icons.event + : Icons.pending; + + final Map stokBantuan = + penyaluran['stok_bantuan'] as Map; + + return Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Baris atas dengan status dan tanggal + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Status penyaluran + Row( + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + statusIcon, + color: statusColor, + size: 16, + ), + ), + const SizedBox(width: 8), + Text( + penyaluran['status'] == 'TERLAKSANA' + ? 'Terlaksana' + : penyaluran['status'] == 'DIJADWALKAN' + ? 'Terjadwal' + : 'Menunggu', + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + // Tanggal penyaluran + Row( + children: [ + const Icon( + Icons.calendar_today, + size: 16, + color: Colors.grey, + ), + const SizedBox(width: 8), + Text( + formattedDate, + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ), + ], + ), + + const Divider(height: 24), + + // Informasi bantuan + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Ikon bantuan + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.inventory_2_outlined, + color: AppTheme.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 16), + + // Detail bantuan + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stokBantuan['nama'] ?? 'Bantuan', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + '${stokBantuan['jenis'] ?? 'Umum'} • ${stokBantuan['kuantitas'] ?? '1 Paket'}', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Text( + penyaluran['keterangan'] ?? '', + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + + // Tampilkan bukti penyaluran jika ada dan status TERLAKSANA + if (penyaluran['status'] == 'TERLAKSANA' && + penyaluran['bukti_penyaluran'] != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 24), + const Text( + 'Bukti Penyaluran', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + penyaluran['bukti_penyaluran'], + height: 120, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 120, + width: double.infinity, + color: Colors.grey[200], + child: const Center( + child: Text('Gambar tidak tersedia'), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } } 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 19c30c2..1aea99e 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -93,6 +93,7 @@ class PetugasDesaView extends GetView { ), ], ); + // Tampilkan tombol riwayat hanya jika tab Penitipan aktif if (activeTab == 2) { return Row( @@ -167,149 +168,336 @@ class PetugasDesaView extends GetView { Widget _buildDrawer(BuildContext context) { return Drawer( - child: ListView( - padding: EdgeInsets.zero, + child: Column( children: [ - DrawerHeader( + 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, - mainAxisAlignment: MainAxisAlignment.end, children: [ - CircleAvatar( - radius: 30, - backgroundColor: Colors.white, - child: const Icon( - Icons.person, - size: 40, - color: AppTheme.primaryColor, + 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 + ? NetworkImage(controller.profilePhotoUrl!) + : null, + child: controller.profilePhotoUrl == null + ? Icon( + Icons.person, + color: Colors.white, + size: 40, + ) + : null, + ), + ), + ), + SizedBox(height: 16), + Text( + 'Halo,', + style: TextStyle( + color: Colors.white70, + fontSize: 16, ), ), - const SizedBox(height: 10), Text( controller.nama, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - Text( - controller.user?.desa?.nama != null - ? '${controller.formattedRole} - ${controller.user!.desa!.nama}' - : controller.formattedRole, style: TextStyle( - color: Colors.white.withAlpha(200), - fontSize: 14, + 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, + ), + ), + ], + ), + ), + ], ), ], ), ), - ListTile( - leading: const Icon(Icons.dashboard_outlined), - title: const Text('Dashboard'), - selected: controller.activeTabIndex.value == 0, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - controller.changeTab(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, + badge: controller.jumlahDiproses.value > 0 + ? controller.jumlahDiproses.value.toString() + : null, + 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.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'); + }, + ), + _buildMenuItem( + icon: Icons.logout, + title: 'Keluar', + onTap: () { + Navigator.pop(context); + controller.logout(); + }, + isLogout: true, + ), + ], + ), ), - ListTile( - leading: const Icon(Icons.handshake_outlined), - title: const Text('Penyaluran'), - selected: controller.activeTabIndex.value == 1, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - controller.changeTab(1); - }, - ), - ListTile( - leading: const Icon(Icons.inventory_2_outlined), - title: const Text('Penitipan'), - selected: controller.activeTabIndex.value == 2, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - controller.changeTab(2); - }, - ), - Obx(() => ListTile( - leading: controller.jumlahDiproses.value > 0 - ? Badge( - label: Text(controller.jumlahDiproses.value.toString()), - backgroundColor: Colors.red, - child: const Icon(Icons.warning_amber_outlined), - ) - : const Icon(Icons.warning_amber_outlined), - title: const Text('Pengaduan'), - selected: controller.activeTabIndex.value == 3, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - controller.changeTab(3); - }, - )), - ListTile( - leading: const Icon(Icons.inventory_outlined), - title: const Text('Stok Bantuan'), - selected: controller.activeTabIndex.value == 4, - selectedColor: AppTheme.primaryColor, - onTap: () { - Navigator.pop(context); - controller.changeTab(4); - }, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.person_add_outlined), - title: const Text('Kelola Penerima'), - onTap: () { - Navigator.pop(context); - Get.toNamed('/daftar-penerima'); - }, - ), - ListTile( - leading: const Icon(Icons.people_outlined), - title: const Text('Kelola Donatur'), - onTap: () { - Navigator.pop(context); - Get.toNamed('/daftar-donatur'); - }, - ), - ListTile( - leading: const Icon(Icons.description_outlined), - title: const Text('Laporan Penyaluran'), - onTap: () { - Navigator.pop(context); - Get.toNamed('/laporan-penyaluran'); - }, - ), - ListTile( - leading: const Icon(Icons.person_outline), - title: const Text('Profil'), - onTap: () { - Navigator.pop(context); - Get.toNamed('/profile'); - }, - ), - ListTile( - leading: const Icon(Icons.logout), - title: const Text('Keluar'), - onTap: () { - Navigator.pop(context); - controller.logout(); - }, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + '© ${DateTime.now().year} Aplikasi Penyaluran Bantuan', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), ), ], ), ); } + 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, + ), + ), + ), + ], + ), + 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, + ), + ); + } + Widget _buildBottomNavigationBar() { return Obx(() { return BottomNavigationBar( @@ -456,29 +644,31 @@ class PetugasDesaView extends GetView { children: [ const Icon(Icons.report_problem_outlined), // Selalu tampilkan badge untuk debugging - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints( - minWidth: 12, - minHeight: 12, - ), - child: Text( - controller.jumlahDiproses.value.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 8, + + if (controller.jumlahDiproses.value > 0) + Positioned( + top: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 12, + minHeight: 12, + ), + child: Text( + controller.jumlahDiproses.value.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 8, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), - ), ], ), activeIcon: Stack( diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart index 2d998c1..75756e0 100644 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -3,16 +3,24 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; class ProfileController extends GetxController { final SupabaseService _supabaseService = Get.find(); final AuthController _authController = Get.find(); + final ImagePicker _imagePicker = ImagePicker(); final Rx user = Rx(null); final RxBool isLoading = true.obs; final RxBool isEditing = false.obs; final Rx?> roleData = Rx?>(null); + // Untuk foto profil + final RxString fotoProfil = ''.obs; + final RxString fotoProfilPath = ''.obs; + final RxBool isUploadingFoto = false.obs; + // Form controllers late TextEditingController nameController; late TextEditingController emailController; @@ -51,10 +59,15 @@ class ProfileController extends GetxController { if (userData['role_data'] != null) { roleData.value = userData['role_data'] as Map?; // Jika role adalah warga, ambil no telepon dari role data - if (user.value?.role?.toLowerCase() == 'warga' && - roleData.value?['no_hp'] != null) { + if (roleData.value?['no_hp'] != null) { phoneController.text = roleData.value?['no_hp'] ?? ''; } + + // Ambil foto profil jika ada + if (roleData.value?['foto_profil'] != null) { + fotoProfil.value = roleData.value?['foto_profil'] ?? ''; + print(fotoProfil.value); + } } } } catch (e) { @@ -74,6 +87,86 @@ class ProfileController extends GetxController { isEditing.value = !isEditing.value; } + // Metode untuk memilih foto profil dari kamera + Future pickFotoProfilFromCamera() async { + try { + final pickedFile = await _imagePicker.pickImage( + source: ImageSource.camera, + imageQuality: 80, + maxWidth: 1000, + maxHeight: 1000, + ); + + if (pickedFile != null) { + fotoProfilPath.value = pickedFile.path; + } + } catch (e) { + print('Error mengambil foto dari kamera: $e'); + Get.snackbar( + 'Error', + 'Gagal mengambil foto: ${e.toString()}', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Metode untuk memilih foto profil dari galeri + Future pickFotoProfilFromGallery() async { + try { + final pickedFile = await _imagePicker.pickImage( + source: ImageSource.gallery, + imageQuality: 80, + maxWidth: 1000, + maxHeight: 1000, + ); + + if (pickedFile != null) { + fotoProfilPath.value = pickedFile.path; + } + } catch (e) { + print('Error mengambil foto dari galeri: $e'); + Get.snackbar( + 'Error', + 'Gagal mengambil foto: ${e.toString()}', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Metode untuk menghapus foto profil + void clearFotoProfil() { + fotoProfilPath.value = ''; + } + + // Metode untuk mengupload foto profil + Future _uploadFotoProfil() async { + if (fotoProfilPath.isEmpty) return null; + + try { + isUploadingFoto.value = true; + final userData = user.value; + if (userData == null) throw 'Data user tidak ditemukan'; + + // Upload foto ke Supabase storage + final fotoUrl = await _supabaseService.uploadFile( + fotoProfilPath.value, + 'profiles', // bucket name + 'profile_photos/${userData.id}', // folder path + ); + + return fotoUrl; + } catch (e) { + print('Error upload foto profil: $e'); + throw e.toString(); + } finally { + isUploadingFoto.value = false; + } + } + Future updateProfile() async { if (nameController.text.isEmpty) { Get.snackbar( @@ -91,6 +184,15 @@ class ProfileController extends GetxController { final userData = user.value; if (userData == null) throw 'Data user tidak ditemukan'; + // Upload foto profil jika ada + String? fotoProfilUrl; + if (fotoProfilPath.isNotEmpty) { + fotoProfilUrl = await _uploadFotoProfil(); + if (fotoProfilUrl == null) { + throw 'Gagal mengupload foto profil'; + } + } + // Update data sesuai role switch (userData.role?.toLowerCase() ?? 'unknown') { case 'warga': @@ -99,6 +201,7 @@ class ProfileController extends GetxController { namaLengkap: nameController.text, noHp: phoneController.text, email: emailController.text, + fotoProfil: fotoProfilUrl, ); break; case 'donatur': @@ -107,6 +210,7 @@ class ProfileController extends GetxController { nama: nameController.text, noHp: phoneController.text, email: emailController.text, + fotoProfil: fotoProfilUrl, ); break; case 'petugas_desa': @@ -115,6 +219,7 @@ class ProfileController extends GetxController { nama: nameController.text, noHp: phoneController.text, email: emailController.text, + fotoProfil: fotoProfilUrl, ); break; default: @@ -127,6 +232,9 @@ class ProfileController extends GetxController { // Refresh data di AuthController untuk menyebarkan perubahan ke seluruh aplikasi await _authController.refreshUserData(); + // Reset path foto setelah update + fotoProfilPath.value = ''; + // Keluar dari mode edit isEditing.value = false; diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart index 6028d49..e630d05 100644 --- a/lib/app/modules/profile/views/profile_view.dart +++ b/lib/app/modules/profile/views/profile_view.dart @@ -2,10 +2,55 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/profile/controllers/profile_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; +import 'dart:io'; class ProfileView extends GetView { const ProfileView({super.key}); + // Helper untuk mengkonversi nilai role ke tampilan yang lebih baik + String _formatRoleName(String? role) { + if (role == null) return 'Pengguna'; + + switch (role.toLowerCase()) { + case 'warga': + return 'Warga'; + case 'petugas_desa': + return 'Petugas Desa'; + case 'admin_desa': + return 'Admin Desa'; + case 'donatur': + return 'Donatur'; + case 'admin': + return 'Administrator'; + default: + // Kapitalisasi setiap kata dan ganti underscore dengan spasi + return role + .split('_') + .map((word) => word.isEmpty + ? '' + : '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}') + .join(' '); + } + } + + // Helper untuk mendapatkan warna berdasarkan role + Color _getRoleColor(String? role) { + if (role == null) return AppTheme.primaryColor; + + switch (role.toLowerCase()) { + case 'warga': + return AppTheme.infoColor; + case 'petugas_desa': + return AppTheme.primaryColor; + + case 'donatur': + return AppTheme.successColor; + + default: + return AppTheme.primaryColor; + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -53,15 +98,71 @@ class ProfileView extends GetView { return Center( child: Column( children: [ - const CircleAvatar( - radius: 50, - backgroundColor: AppTheme.primaryColor, - child: Icon( - Icons.person, - size: 60, - color: Colors.white, - ), - ), + Obx(() { + // Jika user sedang dalam mode edit dan sudah memilih foto baru + if (controller.isEditing.value && + controller.fotoProfilPath.isNotEmpty) { + return _buildProfileImage( + isLocalFile: true, + imagePath: controller.fotoProfilPath.value, + ); + } + // Jika user sudah memiliki foto profil + else if (controller.fotoProfil.isNotEmpty) { + return _buildProfileImage( + isLocalFile: false, + imagePath: controller.fotoProfil.value, + ); + } + // Default jika tidak ada foto + else { + return _buildDefaultProfileImage(); + } + }), + + // Tombol edit foto (hanya muncul dalam mode edit) + Obx(() { + if (controller.isEditing.value) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Tombol ambil dari kamera + _buildPhotoActionButton( + icon: Icons.camera_alt, + label: 'Kamera', + onPressed: () => controller.pickFotoProfilFromCamera(), + ), + const SizedBox(width: 8), + + // Tombol ambil dari galeri + _buildPhotoActionButton( + icon: Icons.photo_library, + label: 'Galeri', + onPressed: () => controller.pickFotoProfilFromGallery(), + ), + + // Tombol hapus foto (hanya jika ada foto yang dipilih) + if (controller.fotoProfilPath.isNotEmpty || + controller.fotoProfil.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: _buildPhotoActionButton( + icon: Icons.delete, + label: 'Hapus', + onPressed: () => controller.clearFotoProfil(), + color: Colors.red, + ), + ), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + const SizedBox(height: 16), Obx(() => Text( controller.user.value?.name ?? 'Pengguna', @@ -71,19 +172,151 @@ class ProfileView extends GetView { ), )), const SizedBox(height: 4), - Obx(() => Text( - controller.user.value?.role?.toUpperCase() ?? 'PENGGUNA', + Obx(() { + final role = controller.user.value?.role; + final roleColor = _getRoleColor(role); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: roleColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: roleColor.withOpacity(0.3)), + ), + child: Text( + _formatRoleName(role), style: TextStyle( fontSize: 14, - color: Colors.grey[600], + color: roleColor, fontWeight: FontWeight.w500, ), - )), + ), + ); + }), ], ), ); } + // Widget foto profil default + Widget _buildDefaultProfileImage() { + return CircleAvatar( + radius: 60, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: const Icon( + Icons.person, + size: 70, + color: AppTheme.primaryColor, + ), + ); + } + + // Widget foto profil dengan gambar + Widget _buildProfileImage( + {required bool isLocalFile, required String imagePath}) { + return Stack( + children: [ + // Widget untuk menampilkan foto profil + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.5), + width: 3, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(60), + child: isLocalFile + ? Image.file( + File(imagePath), + fit: BoxFit.cover, + width: 120, + height: 120, + errorBuilder: (context, error, stackTrace) => + _buildDefaultProfileImage(), + ) + : Image.network( + imagePath, + fit: BoxFit.cover, + width: 120, + height: 120, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) => + _buildDefaultProfileImage(), + ), + ), + ), + + // Indikator loading saat mengupload + if (controller.isUploadingFoto.value) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black38, + shape: BoxShape.circle, + ), + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + ), + ), + ], + ); + } + + // Widget tombol aksi foto + Widget _buildPhotoActionButton({ + required IconData icon, + required String label, + required VoidCallback onPressed, + Color? color, + }) { + final buttonColor = color ?? AppTheme.primaryColor; + + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: buttonColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: buttonColor.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: buttonColor), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: buttonColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + Widget _buildProfileForm() { return Obx(() { final isEditing = controller.isEditing.value; @@ -222,9 +455,6 @@ class ProfileView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow(Icons.badge, 'NIP', roleData?['nip'] ?? '-'), - const SizedBox(height: 8), - _buildInfoRow( - Icons.work, 'Jabatan', roleData?['jabatan'] ?? '-'), if (user.desa != null) ...[ const SizedBox(height: 8), _buildInfoRow( diff --git a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart index 30cde09..b13ad0a 100644 --- a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart +++ b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart @@ -3,6 +3,7 @@ import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart'; +import 'package:penyaluran_app/app/data/models/warga_model.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,9 @@ class WargaDashboardController extends GetxController { final Rx currentUser = Rx(null); + // Variabel untuk foto profil + final RxString fotoProfil = ''.obs; + // Indeks tab yang aktif di bottom navigation bar final RxInt activeTabIndex = 0.obs; @@ -55,6 +59,48 @@ class WargaDashboardController extends GetxController { String? get desa => user?.desa?.nama; + // Getter untuk alamat dan noHp + String? get alamat { + if (_authController.isWarga && _authController.roleData != null) { + return (_authController.roleData as WargaModel).alamat; + } + return null; + } + + String? get noHp { + if (_authController.isWarga && _authController.roleData != null) { + return (_authController.roleData as WargaModel).noHp; + } + return null; + } + + // Getter untuk foto profil + String? get profilePhotoUrl { + // 1. Coba ambil dari fotoProfil yang sudah disimpan + if (fotoProfil.isNotEmpty) { + return fotoProfil.value; + } + + // 2. Coba ambil dari roleData jika merupakan WargaModel + if (_authController.isWarga && _authController.roleData != null) { + final wargaData = _authController.roleData as WargaModel; + if (wargaData.fotoProfil != null && wargaData.fotoProfil!.isNotEmpty) { + return wargaData.fotoProfil; + } + } + + // 3. Coba ambil dari userData.roleData.fotoProfil + final userData = _authController.userData; + if (userData != null && userData.roleData is WargaModel) { + final wargaData = userData.roleData as WargaModel; + if (wargaData.fotoProfil != null && wargaData.fotoProfil!.isNotEmpty) { + return wargaData.fotoProfil; + } + } + + return null; + } + @override void onInit() { super.onInit(); @@ -81,12 +127,45 @@ class WargaDashboardController extends GetxController { print('DEBUG WARGA: User adalah warga'); var wargaData = _authController.roleData; print('DEBUG WARGA: Data warga: ${wargaData?.namaLengkap}'); + + // Ambil foto profil dari wargaData jika ada + if (wargaData != null && + wargaData.fotoProfil != null && + wargaData.fotoProfil!.isNotEmpty) { + fotoProfil.value = wargaData.fotoProfil!; + print('DEBUG WARGA: Foto profil: ${fotoProfil.value}'); + } } else { print('DEBUG WARGA: User bukan warga'); } } else { print('DEBUG WARGA: userData null'); } + + // Cek dan ambil foto profil jika belum ada + if (fotoProfil.isEmpty) { + _fetchProfilePhoto(); + } + } + + // Metode untuk mengambil foto profil + Future _fetchProfilePhoto() async { + try { + if (user?.id == null) return; + + final wargaData = await _supabaseService.client + .from('warga') + .select('foto_profil') + .eq('user_id', user!.id) + .single(); + + if (wargaData != null && wargaData['foto_profil'] != null) { + fotoProfil.value = wargaData['foto_profil']; + print('DEBUG WARGA: Foto profil dari API: ${fotoProfil.value}'); + } + } catch (e) { + print('Error fetching profile photo: $e'); + } } void fetchData() async { diff --git a/lib/app/modules/warga/views/warga_dashboard_view.dart b/lib/app/modules/warga/views/warga_dashboard_view.dart index 5e6f638..02e1cc8 100644 --- a/lib/app/modules/warga/views/warga_dashboard_view.dart +++ b/lib/app/modules/warga/views/warga_dashboard_view.dart @@ -41,111 +41,256 @@ class WargaDashboardView extends GetView { } Widget _buildWelcomeSection() { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - radius: 24, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - color: Colors.blue.shade700, - size: 28, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Selamat Datang,', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - ), - ), - Text( - controller.nama, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [Colors.white, Colors.blue.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.home, - size: 16, - color: Colors.grey.shade600, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Alamat tidak tersedia', - style: TextStyle( - color: Colors.grey.shade700, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.blue.shade200, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Hero( + tag: 'warga-profile', + child: CircleAvatar( + radius: 30, + backgroundColor: Colors.blue.shade100, + backgroundImage: controller.profilePhotoUrl != null + ? NetworkImage(controller.profilePhotoUrl!) + : null, + child: controller.profilePhotoUrl == null + ? Icon( + Icons.person, + color: Colors.blue.shade700, + size: 30, + ) + : null, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat Datang,', + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade700, + fontWeight: FontWeight.w500, + ), + ), + Text( + controller.nama, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue.shade900, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.phone, - size: 16, + child: Column( + children: [ + _buildInfoRow( + icon: Icons.home_rounded, + iconColor: Colors.blue.shade300, + label: 'Alamat', + value: controller.alamat ?? 'Alamat tidak tersedia', + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1), + ), + _buildInfoRow( + icon: Icons.phone_rounded, + iconColor: Colors.green.shade300, + label: 'No. HP', + value: controller.noHp ?? 'No. HP tidak tersedia', + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Divider(height: 1), + ), + _buildInfoRow( + icon: Icons.location_city_rounded, + iconColor: Colors.amber.shade300, + label: 'Desa', + value: controller.desa ?? 'Desa tidak tersedia', + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildActionButton( + icon: Icons.edit_rounded, + label: 'Edit Profil', + color: Colors.blue.shade700, + onTap: () => Get.toNamed(Routes.PROFILE), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildActionButton( + icon: Icons.notifications_rounded, + label: 'Notifikasi', + color: Colors.amber.shade700, + onTap: () => Get.toNamed(Routes.NOTIFIKASI), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoRow({ + required IconData icon, + required Color iconColor, + required String label, + required String value, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + size: 16, + color: iconColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, color: Colors.grey.shade600, + fontWeight: FontWeight.w500, ), - const SizedBox(width: 8), - Text( - 'No. HP tidak tersedia', - style: TextStyle( - color: Colors.grey.shade700, - ), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.location_city, - size: 16, - color: Colors.grey.shade600, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Ink( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 18, + color: color, + ), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + color: color, + fontWeight: FontWeight.w600, ), - const SizedBox(width: 8), - Text( - controller.desa ?? 'Desa tidak tersedia', - style: TextStyle( - color: Colors.grey.shade700, - ), - ), - ], - ), - ], + ), + ], + ), ), ), ); diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 19cdcee..cd5b159 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -33,6 +33,11 @@ abstract class Routes { static const riwayatPengaduan = _Paths.riwayatPengaduan; static const qrScanner = _Paths.qrScanner; static const konfirmasiPenerimaQr = _Paths.konfirmasiPenerimaQr; + static const PENGADUAN = '/pengaduan'; + static const TAMBAH_PENGADUAN = '/tambah-pengaduan'; + static const PENGADUAN_DETAIL = '/pengaduan-detail'; + static const PROFILE = '/profile'; + static const NOTIFIKASI = '/notifikasi'; } abstract class _Paths { @@ -68,4 +73,9 @@ 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 PENGADUAN = '/pengaduan'; + static const TAMBAH_PENGADUAN = '/tambah-pengaduan'; + static const PENGADUAN_DETAIL = '/pengaduan-detail'; + static const PROFILE = '/profile'; + static const NOTIFIKASI = '/notifikasi'; } diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index 14ecb6c..d901675 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -911,7 +911,7 @@ class SupabaseService extends GetxService { final response = await client .from('donatur') .select('*') - .order('nama', ascending: true); + .order('nama_lengkap', ascending: true); return response; } catch (e) { @@ -1105,6 +1105,23 @@ class SupabaseService extends GetxService { } } + // Metode untuk mendapatkan data penyaluran bantuan berdasarkan ID warga + Future>?> getPenyaluranBantuanByWargaId( + String wargaId) async { + try { + final response = await client + .from('penyaluran_bantuan') + .select('*, stok_bantuan:stok_bantuan_id(*)') + .eq('penerima_id', wargaId) + .order('tanggal_penyaluran', ascending: false); + + return response; + } catch (e) { + print('Error getting penyaluran bantuan by warga id: $e'); + return null; + } + } + Future tambahPenerima(Map penerima) async { try { await client.from('warga').insert(penerima); @@ -1616,103 +1633,108 @@ class SupabaseService extends GetxService { Future updateWargaProfile({ required String userId, required String namaLengkap, + required String email, String? noHp, - String? email, - String? alamat, - String? nik, - String? tempatLahir, - DateTime? tanggalLahir, - String? jenisKelamin, - String? agama, - String? kategoriEkonomi, + String? fotoProfil, }) async { try { - final data = { + // Buat map untuk update data + final Map updateData = { 'nama_lengkap': namaLengkap, + 'no_hp': noHp, 'updated_at': DateTime.now().toIso8601String(), }; - if (noHp != null) data['no_hp'] = noHp; - if (email != null) data['email'] = email; - if (alamat != null) data['alamat'] = alamat; - if (nik != null) data['nik'] = nik; - if (tempatLahir != null) data['tempat_lahir'] = tempatLahir; - if (tanggalLahir != null) - data['tanggal_lahir'] = tanggalLahir.toIso8601String(); - if (jenisKelamin != null) data['jenis_kelamin'] = jenisKelamin; - if (agama != null) data['agama'] = agama; - if (kategoriEkonomi != null) data['kategori_ekonomi'] = kategoriEkonomi; + // Tambahkan foto profil jika ada + if (fotoProfil != null) { + updateData['foto_profil'] = fotoProfil; + } - await client.from('warga').update(data).eq('id', userId); + // Update data warga + await client.from('warga').update(updateData).eq('id', userId); - // Hapus cache setelah update - clearUserProfileCache(); + // Update email di auth.users jika berubah + if (email != client.auth.currentUser?.email) { + // Gunakan metode updateUserEmail + await client.auth.updateUser(UserAttributes( + email: email, + )); + } } catch (e) { print('Error updating warga profile: $e'); throw e.toString(); } } - // Metode untuk update profil donatur + // Metode untuk memperbarui profil donatur Future updateDonaturProfile({ required String userId, required String nama, - String? alamat, + required String email, String? noHp, - String? email, - String? jenis, - String? instansi, - String? jabatan, + String? fotoProfil, }) async { try { - final data = { + // Buat map untuk update data + final Map updateData = { 'nama': nama, + 'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap + 'no_hp': noHp, 'updated_at': DateTime.now().toIso8601String(), }; - if (alamat != null) data['alamat'] = alamat; - if (noHp != null) data['no_hp'] = noHp; - if (email != null) data['email'] = email; - if (jenis != null) data['jenis'] = jenis; - if (instansi != null) data['instansi'] = instansi; - if (jabatan != null) data['jabatan'] = jabatan; + // Tambahkan foto profil jika ada + if (fotoProfil != null) { + updateData['foto_profil'] = fotoProfil; + } - await client.from('donatur').update(data).eq('id', userId); + // Update data donatur + await client.from('donatur').update(updateData).eq('id', userId); - // Hapus cache setelah update - clearUserProfileCache(); + // Update email di auth.users jika berubah + if (email != client.auth.currentUser?.email) { + // Gunakan metode updateUserEmail + await client.auth.updateUser(UserAttributes( + email: email, + )); + } } catch (e) { print('Error updating donatur profile: $e'); throw e.toString(); } } - // Metode untuk update profil petugas desa + // Metode untuk memperbarui profil petugas desa Future updatePetugasDesaProfile({ required String userId, required String nama, - String? alamat, + required String email, String? noHp, - String? email, - String? nip, - String? jabatan, + String? fotoProfil, }) async { try { - final data = { - 'nama': nama, + // Buat map untuk update data + final Map updateData = { + 'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap + 'no_hp': noHp, 'updated_at': DateTime.now().toIso8601String(), }; - if (alamat != null) data['alamat'] = alamat; - if (noHp != null) data['no_hp'] = noHp; - if (email != null) data['email'] = email; - if (nip != null) data['nip'] = nip; - if (jabatan != null) data['jabatan'] = jabatan; + // Tambahkan foto profil jika ada + if (fotoProfil != null) { + updateData['foto_profil'] = fotoProfil; + } - await client.from('petugas_desa').update(data).eq('id', userId); + // Update data petugas desa + await client.from('petugas_desa').update(updateData).eq('id', userId); - // Hapus cache setelah update - clearUserProfileCache(); + // Update email di auth.users jika berubah + if (email != client.auth.currentUser?.email) { + // Gunakan metode updateUserEmail + await client.auth.updateUser(UserAttributes( + email: email, + )); + } } catch (e) { print('Error updating petugas desa profile: $e'); throw e.toString();