From 3b12c7af86204ea41f60a467925a41f724c6d7a1 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Wed, 19 Mar 2025 22:57:42 +0700 Subject: [PATCH] Perbarui model dan tampilan untuk mendukung status penyaluran dalam aplikasi. Tambahkan properti statusPenyaluran pada PenerimaPenyaluranModel dan SkemaBantuanModel. Modifikasi tampilan di BantuanCard dan StatusBadge untuk menampilkan status penyaluran dengan lebih baik. Hapus penggunaan prioritas di beberapa model dan tampilan untuk menyederhanakan kode. Implementasikan logika baru di JadwalPenyaluranController untuk memperbarui stok bantuan berdasarkan jumlah yang diterima. --- .../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 +- .../models/penerima_penyaluran_model.dart | 4 + lib/app/data/models/skema_bantuan_model.dart | 6 + .../data/models/tindakan_pengaduan_model.dart | 22 +- .../jadwal_penyaluran_controller.dart | 27 + .../controllers/pengaduan_controller.dart | 4 - .../views/detail_pengaduan_view.dart | 150 ++- .../views/detail_penyaluran_page.dart | 10 +- .../petugas_desa/views/pengaduan_view.dart | 5 +- .../views/pengaduan_view_backup.dart | 657 ---------- .../views/tambah_penyaluran_view.dart | 710 ++++++++--- .../warga_dashboard_controller.dart | 110 +- .../warga/views/detail_pengaduan_view.dart | 24 - .../views/warga_detail_penerimaan_view.dart | 1068 +++++++++++++++-- .../warga/views/warga_penerimaan_view.dart | 16 +- .../warga/views/warga_pengaduan_view.dart | 21 +- lib/app/modules/warga/views/warga_view.dart | 24 - lib/app/widgets/bantuan_card.dart | 70 +- lib/app/widgets/status_badge.dart | 8 + 22 files changed, 1886 insertions(+), 1146 deletions(-) delete mode 100644 lib/app/modules/petugas_desa/views/pengaduan_view_backup.dart 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 36b5223..9fe891c 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  NJ2 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  NJ2  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  NJ2 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  NJ2 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  NJ2 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  NJ2y +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  NJ2 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  NJ2 ~ +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  NJ2 +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  NJ2  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  NJ2  ( 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  NJ2  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 8112d56..f43910d 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 7781258..79fa7b2 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 bf13839..1b52820 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/penerima_penyaluran_model.dart b/lib/app/data/models/penerima_penyaluran_model.dart index 6264c56..6531f2c 100644 --- a/lib/app/data/models/penerima_penyaluran_model.dart +++ b/lib/app/data/models/penerima_penyaluran_model.dart @@ -23,6 +23,7 @@ class PenerimaPenyaluranModel { final String? lokasiPenyaluranNama; // Nama lokasi penyaluran final String? lokasiPenyaluranAlamat; // Alamat lokasi penyaluran final String? qrCodeHash; // Hash untuk QR code + final String? statusPenyaluran; // Status penyaluran PenerimaPenyaluranModel({ this.id, @@ -47,6 +48,7 @@ class PenerimaPenyaluranModel { this.lokasiPenyaluranNama, this.lokasiPenyaluranAlamat, this.qrCodeHash, + this.statusPenyaluran, }); factory PenerimaPenyaluranModel.fromRawJson(String str) => @@ -82,6 +84,7 @@ class PenerimaPenyaluranModel { lokasiPenyaluranNama: json["lokasi_penyaluran_nama"], lokasiPenyaluranAlamat: json["lokasi_penyaluran_alamat"], qrCodeHash: json["qr_code_hash"], + statusPenyaluran: json["status_penyaluran"], ); Map toJson() => { @@ -107,5 +110,6 @@ class PenerimaPenyaluranModel { "lokasi_penyaluran_nama": lokasiPenyaluranNama, "lokasi_penyaluran_alamat": lokasiPenyaluranAlamat, "qr_code_hash": qrCodeHash, + "status_penyaluran": statusPenyaluran, }; } diff --git a/lib/app/data/models/skema_bantuan_model.dart b/lib/app/data/models/skema_bantuan_model.dart index 0377efc..9d19e54 100644 --- a/lib/app/data/models/skema_bantuan_model.dart +++ b/lib/app/data/models/skema_bantuan_model.dart @@ -11,6 +11,7 @@ class SkemaBantuanModel { final DateTime? updatedAt; final String? stokBantuanId; final String? kategoriBantuanId; + final double? jumlahDiterimaPerOrang; SkemaBantuanModel({ this.id, @@ -23,6 +24,7 @@ class SkemaBantuanModel { this.updatedAt, this.stokBantuanId, this.kategoriBantuanId, + this.jumlahDiterimaPerOrang, }); factory SkemaBantuanModel.fromRawJson(String str) => @@ -46,6 +48,9 @@ class SkemaBantuanModel { : null, stokBantuanId: json["stok_bantuan_id"], kategoriBantuanId: json["kategori_bantuan_id"], + jumlahDiterimaPerOrang: json["jumlah_diterima_per_orang"] != null + ? json["jumlah_diterima_per_orang"].toDouble() + : null, ); Map toJson() => { @@ -59,5 +64,6 @@ class SkemaBantuanModel { "updated_at": updatedAt?.toIso8601String(), "stok_bantuan_id": stokBantuanId, "kategori_bantuan_id": kategoriBantuanId, + "jumlah_diterima_per_orang": jumlahDiterimaPerOrang, }; } diff --git a/lib/app/data/models/tindakan_pengaduan_model.dart b/lib/app/data/models/tindakan_pengaduan_model.dart index 6121112..c793516 100644 --- a/lib/app/data/models/tindakan_pengaduan_model.dart +++ b/lib/app/data/models/tindakan_pengaduan_model.dart @@ -6,7 +6,6 @@ class TindakanPengaduanModel { final String? tindakan; final String? catatan; final String? statusTindakan; // PROSES, SELESAI - final String? prioritas; // RENDAH, SEDANG, TINGGI final String? kategoriTindakan; // Kategori tindakan enum final String? petugasId; final String? verifikatorId; @@ -27,7 +26,6 @@ class TindakanPengaduanModel { this.tindakan, this.catatan, this.statusTindakan, - this.prioritas, this.kategoriTindakan, this.petugasId, this.verifikatorId, @@ -55,7 +53,6 @@ class TindakanPengaduanModel { tindakan: json["tindakan"], catatan: json["catatan"], statusTindakan: json["status_tindakan"], - prioritas: json["prioritas"], kategoriTindakan: json["kategori_tindakan"], petugasId: json["petugas_id"], verifikatorId: json["verifikator_id"], @@ -76,9 +73,7 @@ class TindakanPengaduanModel { updatedAt: json["updated_at"] != null ? DateTime.parse(json["updated_at"]) : null, - biayaTindakan: json["biaya_tindakan"] != null - ? double.parse(json["biaya_tindakan"].toString()) - : null, + biayaTindakan: json["biaya_tindakan"]?.toDouble(), petugas: json["petugas"], verifikator: json["verifikator"], ); @@ -89,7 +84,6 @@ class TindakanPengaduanModel { "tindakan": tindakan, "catatan": catatan, "status_tindakan": statusTindakan, - "prioritas": prioritas, "kategori_tindakan": kategoriTindakan, "petugas_id": petugasId, "verifikator_id": verifikatorId, @@ -137,20 +131,6 @@ class TindakanPengaduanModel { } } - // Getter untuk mendapatkan prioritas yang lebih user-friendly - String get prioritasText { - switch (prioritas) { - case 'RENDAH': - return 'Prioritas Rendah'; - case 'SEDANG': - return 'Prioritas Sedang'; - case 'TINGGI': - return 'Prioritas Tinggi'; - default: - return prioritas ?? 'Tidak Diketahui'; - } - } - // Getter untuk mendapatkan kategori tindakan yang lebih user-friendly String get kategoriTindakanText { if (kategoriTindakan == null) return 'Tidak Diketahui'; diff --git a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart index 9de570e..5f11f5b 100644 --- a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -383,6 +383,9 @@ class JadwalPenyaluranController extends GetxController { required String lokasiPenyaluranId, required int jumlahPenerima, required DateTime? tanggalPenyaluran, + required double jumlahDiterimaPerOrang, + required String stokBantuanId, + required double totalStokDibutuhkan, }) async { isLoading.value = true; try { @@ -427,6 +430,7 @@ class JadwalPenyaluranController extends GetxController { 'stok_bantuan_id': skemaBantuanCache[skemaId]?.stokBantuanId, 'status_penerimaan': 'BELUMMENERIMA', 'qr_code_hash': qrCodeHash, + 'jumlah_bantuan': jumlahDiterimaPerOrang, }; // Simpan data penerima ke database @@ -435,6 +439,29 @@ class JadwalPenyaluranController extends GetxController { .insert(penerimaPenyaluran); } + // Update stok bantuan (kurangi dengan total stok yang dibutuhkan) + try { + // Dapatkan stok saat ini + final stokData = await _supabaseService.client + .from('stok_bantuan') + .select('total_stok') + .eq('id', stokBantuanId) + .single(); + + if (stokData != null && stokData['total_stok'] != null) { + final currentStok = stokData['total_stok'].toDouble(); + final newStok = currentStok - totalStokDibutuhkan; + + // Update stok bantuan dengan nilai baru + await _supabaseService.client + .from('stok_bantuan') + .update({'total_stok': newStok}).eq('id', stokBantuanId); + } + } catch (e) { + print('Error updating stok bantuan: $e'); + // Tidak throw exception di sini karena penyaluran sudah disimpan + } + // Setelah berhasil menambahkan, refresh data await loadJadwalData(); await loadPermintaanPenjadwalanData(); diff --git a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart index 596fdc3..4a94fc5 100644 --- a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart @@ -108,7 +108,6 @@ class PengaduanController extends GetxController { required String tindakan, required String kategoriTindakan, required String statusTindakan, - required String prioritas, String? catatan, String? hasilTindakan, required List buktiTindakanPaths, @@ -134,7 +133,6 @@ class PengaduanController extends GetxController { 'tindakan': tindakan, 'catatan': catatan, 'status_tindakan': statusTindakan, - 'prioritas': prioritas, 'kategori_tindakan': kategoriTindakan, 'hasil_tindakan': hasilTindakan, 'tanggal_tindakan': DateTime.now().toIso8601String(), @@ -188,7 +186,6 @@ class PengaduanController extends GetxController { required String tindakan, required String kategoriTindakan, required String statusTindakan, - required String prioritas, String? catatan, String? hasilTindakan, required List buktiTindakanPaths, @@ -217,7 +214,6 @@ class PengaduanController extends GetxController { 'tindakan': tindakan, 'catatan': catatan, 'status_tindakan': statusTindakan, - 'prioritas': prioritas, 'kategori_tindakan': kategoriTindakan, 'hasil_tindakan': hasilTindakan, 'bukti_tindakan': buktiTindakanUrls, diff --git a/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart b/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart index f115338..557504e 100644 --- a/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart @@ -460,6 +460,92 @@ class DetailPengaduanView extends GetView { ), ], ), + const SizedBox(height: 12), + if (pengaduan.fotoPengaduan != null && + pengaduan.fotoPengaduan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Foto Pengaduan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: pengaduan.fotoPengaduan!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + // Tampilkan gambar dalam ukuran penuh saat diklik + Get.to(() => Scaffold( + appBar: AppBar( + title: const Text('Foto Pengaduan'), + backgroundColor: Colors.black, + ), + body: Center( + child: InteractiveViewer( + child: Image.network( + pengaduan.fotoPengaduan![index], + fit: BoxFit.contain, + ), + ), + ), + backgroundColor: Colors.black, + )); + }, + child: Container( + width: 120, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + pengaduan.fotoPengaduan![index], + fit: BoxFit.cover, + 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) { + return Container( + color: Colors.grey.shade200, + child: const Center( + child: + Icon(Icons.error, color: Colors.red), + ), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ), const SizedBox(height: 16), // Panel status pengaduan _buildStatusPanel(context, pengaduan), @@ -1234,17 +1320,6 @@ class DetailPengaduanView extends GetView { ), ], ), - - // Prioritas tindakan (jika ada) - if (tindakan.prioritas != null) ...[ - const SizedBox(height: 8), - // Menggunakan StatusPill untuk prioritas tindakan - StatusPill( - status: tindakan.prioritasText, - backgroundColor: _getPriorityColor(tindakan.prioritas), - textColor: Colors.white, - ), - ], // Tampilkan tombol edit jika status PROSES if (tindakan.statusTindakan == 'PROSES') ...[ const SizedBox(height: 8), @@ -1288,24 +1363,10 @@ class DetailPengaduanView extends GetView { ); } - Color _getPriorityColor(String? priority) { - switch (priority) { - case 'TINGGI': - return Colors.red; - case 'SEDANG': - return Colors.orange; - case 'RENDAH': - return Colors.green; - default: - return Colors.grey; - } - } - void _showTambahTindakanDialog(BuildContext context, String pengaduanId) { final formKey = GlobalKey(); final tindakanController = TextEditingController(); String? selectedKategori; - String? selectedPrioritas; String selectedStatus = 'PROSES'; final List kategoriOptions = [ @@ -1326,12 +1387,6 @@ class DetailPengaduanView extends GetView { 'PELAPORAN_KE_PIHAK_BERWENANG', ]; - final List prioritasOptions = [ - 'RENDAH', - 'SEDANG', - 'TINGGI', - ]; - // Konversi ke format DropdownItem final List> kategoriItems = kategoriOptions .map((kategori) => DropdownItem( @@ -1340,15 +1395,6 @@ class DetailPengaduanView extends GetView { )) .toList(); - // Konversi ke format DropdownItem untuk prioritas - final List> prioritasItems = prioritasOptions - .map((prioritas) => DropdownItem( - value: prioritas, - label: prioritas[0].toUpperCase() + - prioritas.substring(1).toLowerCase(), - )) - .toList(); - showDialog( context: context, builder: (context) => AlertDialog( @@ -1380,26 +1426,6 @@ class DetailPengaduanView extends GetView { const SizedBox(height: 16), - // Prioritas menggunakan DropdownInput - DropdownInput( - label: 'Prioritas', - hint: 'Pilih prioritas tindakan', - items: prioritasItems, - value: selectedPrioritas, - onChanged: (value) { - selectedPrioritas = value; - }, - required: true, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Pilih prioritas tindakan'; - } - return null; - }, - ), - - const SizedBox(height: 16), - // Deskripsi tindakan menggunakan TextInput TextInput( label: 'Deskripsi Tindakan', @@ -1433,7 +1459,6 @@ class DetailPengaduanView extends GetView { tindakan: tindakanController.text, kategoriTindakan: selectedKategori ?? '', statusTindakan: selectedStatus, - prioritas: selectedPrioritas ?? '', catatan: null, hasilTindakan: null, buktiTindakanPaths: [], @@ -1466,7 +1491,6 @@ class DetailPengaduanView extends GetView { final hasilTindakanController = TextEditingController(text: tindakan.hasilTindakan); String? selectedKategori = tindakan.kategoriTindakan; - String? selectedPrioritas = tindakan.prioritas; String selectedStatus = 'SELESAI'; // Gunakan List untuk bukti tindakan paths @@ -1865,8 +1889,6 @@ class DetailPengaduanView extends GetView { kategoriTindakan: selectedKategori ?? '', // Gunakan kategori yang sudah ada statusTindakan: selectedStatus, - prioritas: selectedPrioritas ?? - '', // Gunakan prioritas yang sudah ada catatan: catatanController.text.isEmpty ? null : catatanController.text, diff --git a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart index c874dfd..4acbb3e 100644 --- a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart +++ b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart @@ -37,7 +37,7 @@ class DetailPenyaluranPage extends StatelessWidget { title: const Text('Detail Penyaluran'), centerTitle: true, leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), + icon: const Icon(Icons.arrow_back, color: AppTheme.primaryColor), onPressed: () => Get.back(), ), ), @@ -969,8 +969,14 @@ class DetailPenyaluranPage extends StatelessWidget { backgroundColor: AppTheme.successColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), + // Tombol disabled jika belum semua penerima menerima bantuan + disabledBackgroundColor: Colors.grey.shade300, + disabledForegroundColor: Colors.grey.shade700, ), - onPressed: controller.selesaikanPenyaluran, + onPressed: controller.penerimaPenyaluran.every((penerima) => + penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA') + ? controller.selesaikanPenyaluran + : null, ), ), const SizedBox(width: 12), diff --git a/lib/app/modules/petugas_desa/views/pengaduan_view.dart b/lib/app/modules/petugas_desa/views/pengaduan_view.dart index 968f422..5c2fd08 100644 --- a/lib/app/modules/petugas_desa/views/pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/pengaduan_view.dart @@ -321,8 +321,6 @@ class PengaduanView extends GetView { String formattedDate = ''; if (item.tanggalPengaduan != null) { formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan); - } else if (item.createdAt != null) { - formattedDate = DateTimeHelper.formatDate(item.createdAt); } return Container( @@ -452,7 +450,7 @@ class PengaduanView extends GetView { child: _buildItemDetail( context, icon: Icons.calendar_today, - label: 'Tanggal', + label: 'Tanggal Pengaduan', value: formattedDate, ), ), @@ -631,7 +629,6 @@ class PengaduanView extends GetView { tindakan: controller.tindakanController.text, kategoriTindakan: 'VERIFIKASI_DATA', statusTindakan: 'PROSES', - prioritas: 'SEDANG', catatan: controller.catatanController.text.isEmpty ? null : controller.catatanController.text, diff --git a/lib/app/modules/petugas_desa/views/pengaduan_view_backup.dart b/lib/app/modules/petugas_desa/views/pengaduan_view_backup.dart deleted file mode 100644 index f0f95c3..0000000 --- a/lib/app/modules/petugas_desa/views/pengaduan_view_backup.dart +++ /dev/null @@ -1,657 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart'; -import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; - -class PengaduanView extends GetView { - const PengaduanView({super.key}); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Ringkasan pengaduan - _buildPengaduanSummary(context), - - const SizedBox(height: 24), - - // Filter dan pencarian - _buildFilterSearch(context), - - // Informasi terakhir update - _buildLastUpdateInfo(context), - - const SizedBox(height: 20), - - // Daftar pengaduan - _buildPengaduanList(context), - ], - ), - ), - ); - } - - // Tambahkan widget untuk menampilkan waktu terakhir update - Widget _buildLastUpdateInfo(BuildContext context) { - return Obx(() { - final lastUpdate = DateTime - .now(); // Gunakan waktu saat ini atau dari controller jika tersedia - final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate); - - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - Icon(Icons.update, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - 'Data terupdate: $formattedDate', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - ], - ), - ); - }); - } - - Widget _buildPengaduanSummary(BuildContext context) { - return Obx(() { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ringkasan Pengaduan', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildSummaryItem( - context, - icon: Icons.pending_actions, - title: 'Diproses', - value: controller.jumlahDiproses.toString(), - color: Colors.orange, - ), - ), - Expanded( - child: _buildSummaryItem( - context, - icon: Icons.engineering, - title: 'Tindakan', - value: controller.jumlahTindakan.toString(), - color: Colors.blue, - ), - ), - Expanded( - child: _buildSummaryItem( - context, - icon: Icons.check_circle, - title: 'Selesai', - value: controller.jumlahSelesai.toString(), - color: Colors.green, - ), - ), - ], - ), - ], - ), - ); - }); - } - - Widget _buildSummaryItem( - BuildContext context, { - required IconData icon, - required String title, - required String value, - required Color color, - }) { - return Column( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(height: 8), - Text( - value, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Text( - title, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - ], - ); - } - - Widget _buildFilterSearch(BuildContext context) { - return Row( - children: [ - Expanded( - child: TextField( - controller: controller.searchController, - decoration: InputDecoration( - hintText: 'Cari pengaduan...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: const EdgeInsets.symmetric(vertical: 0), - ), - onChanged: (value) { - // Implementasi pencarian - controller.refreshData(); - }, - ), - ), - const SizedBox(width: 12), - Container( - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: PopupMenuButton( - icon: const Icon(Icons.filter_list), - tooltip: 'Filter', - onSelected: (index) { - controller.changeCategory(index); - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 0, - child: Text('Semua'), - ), - const PopupMenuItem( - value: 1, - child: Text('Diproses'), - ), - const PopupMenuItem( - value: 2, - child: Text('Tindakan'), - ), - const PopupMenuItem( - value: 3, - child: Text('Selesai'), - ), - ], - ), - ), - ], - ); - } - - Widget _buildPengaduanList(BuildContext context) { - return Obx(() { - if (controller.isLoading.value) { - return const Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: CircularProgressIndicator(), - ), - ); - } - - final filteredPengaduan = controller.getFilteredPengaduan(); - - if (filteredPengaduan.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Icon( - Icons.inbox_outlined, - size: 80, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Belum ada pengaduan', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.grey.shade600, - ), - ), - ], - ), - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Daftar Pengaduan', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - Text( - '${DateTimeHelper.formatNumber(filteredPengaduan.length)} item', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => controller.refreshData(), - tooltip: 'Refresh', - ), - ], - ), - ], - ), - const SizedBox(height: 12), - ...filteredPengaduan - .map((item) => _buildPengaduanItem(context, item)), - ], - ); - }); - } - - Widget _buildPengaduanItem(BuildContext context, dynamic item) { - Color statusColor; - IconData statusIcon; - - switch (item.status?.toUpperCase()) { - case 'MENUNGGU': - statusColor = AppTheme.warningColor; - statusIcon = Icons.pending; - break; - case 'TINDAKAN': - statusColor = AppTheme.infoColor; - statusIcon = Icons.sync; - break; - case 'SELESAI': - statusColor = AppTheme.successColor; - statusIcon = Icons.check_circle; - break; - default: - statusColor = Colors.grey; - statusIcon = Icons.help_outline; - } - - // Format tanggal menggunakan DateTimeHelper - String formattedDate = ''; - if (item.tanggalPengaduan != null) { - formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan); - } else if (item.createdAt != null) { - formattedDate = DateTimeHelper.formatDate(item.createdAt); - } - - return Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withAlpha(26), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - item.warga?['nama'] ?? item.judul ?? '', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - statusIcon, - size: 16, - color: statusColor, - ), - const SizedBox(width: 4), - Text( - item.status ?? '', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: statusColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 4), - _buildItemDetail( - context, - icon: Icons.person, - label: 'NIK', - value: item.warga?['nik'] ?? '', - ), - const SizedBox(height: 12), - if (item.penerimaPenyaluran != null) ...[ - Row( - children: [ - Expanded( - child: _buildItemDetail( - context, - icon: Icons.category, - label: 'Penyaluran', - value: item.namaPenyaluran ?? '', - ), - ), - Expanded( - child: _buildItemDetail( - context, - icon: Icons.inventory, - label: 'Jenis', - value: item.jenisBantuan ?? '', - ), - ), - ], - ), - const SizedBox(height: 8), - _buildItemDetail( - context, - icon: Icons.shopping_bag, - label: 'Jumlah', - value: item.jumlahBantuan ?? '', - ), - ], - const SizedBox(height: 8), - Text( - item.deskripsi ?? '', - style: Theme.of(context).textTheme.bodyMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 8), - _buildItemDetail( - context, - icon: Icons.calendar_today, - label: 'Tanggal', - value: formattedDate, - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: _buildActionButtons(context, item), - ), - ], - ), - ), - ); - } - - Widget _buildItemDetail( - BuildContext context, { - required IconData icon, - required String label, - required String value, - }) { - return Row( - children: [ - Icon( - icon, - size: 16, - color: Colors.grey, - ), - const SizedBox(width: 4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey, - ), - ), - Text( - value, - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ); - } - - List _buildActionButtons(BuildContext context, dynamic item) { - final status = item.status?.toUpperCase(); - - if (status == 'MENUNGGU') { - return [ - TextButton.icon( - onPressed: () { - // Implementasi untuk memproses pengaduan - _showTindakanDialog(context, item); - }, - icon: const Icon(Icons.engineering, size: 18), - label: const Text('Tindakan'), - style: TextButton.styleFrom( - foregroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - TextButton.icon( - onPressed: () { - // Navigasi ke halaman detail pengaduan - Get.toNamed('/detail-pengaduan', arguments: {'id': item.id}); - }, - icon: const Icon(Icons.info_outline, size: 18), - label: const Text('Detail'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - ]; - } else if (status == 'TINDAKAN') { - return [ - TextButton.icon( - onPressed: () { - // Implementasi untuk menyelesaikan pengaduan - _showSelesaikanDialog(context, item); - }, - icon: const Icon(Icons.check_circle, size: 18), - label: const Text('Selesaikan'), - style: TextButton.styleFrom( - foregroundColor: Colors.green, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - TextButton.icon( - onPressed: () { - // Navigasi ke halaman detail pengaduan - Get.toNamed('/detail-pengaduan', arguments: {'id': item.id}); - }, - icon: const Icon(Icons.info_outline, size: 18), - label: const Text('Detail'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - ]; - } else { - return [ - TextButton.icon( - onPressed: () { - // Navigasi ke halaman detail pengaduan - Get.toNamed('/detail-pengaduan', arguments: {'id': item.id}); - }, - icon: const Icon(Icons.info_outline, size: 18), - label: const Text('Detail'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - ]; - } - } - - void _showTindakanDialog(BuildContext context, dynamic item) { - controller.tindakanController.clear(); - controller.catatanController.clear(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Tindakan Pengaduan'), - content: Form( - key: controller.tindakanFormKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'), - const SizedBox(height: 16), - TextFormField( - controller: controller.tindakanController, - decoration: const InputDecoration( - labelText: 'Tindakan yang dilakukan', - border: OutlineInputBorder(), - ), - maxLines: 3, - validator: controller.validateTindakan, - ), - const SizedBox(height: 12), - TextFormField( - controller: controller.catatanController, - decoration: const InputDecoration( - labelText: 'Catatan (opsional)', - border: OutlineInputBorder(), - ), - maxLines: 2, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Batal'), - ), - ElevatedButton( - onPressed: () { - if (controller.tindakanFormKey.currentState!.validate()) { - Navigator.pop(context); - controller.tambahTindakanPengaduan( - pengaduanId: item.id!, - tindakan: controller.tindakanController.text, - kategoriTindakan: 'VERIFIKASI_DATA', - statusTindakan: 'PROSES', - prioritas: 'SEDANG', - catatan: controller.catatanController.text.isEmpty - ? null - : controller.catatanController.text, - buktiTindakanPaths: [], - ); - } - }, - child: const Text('Simpan'), - ), - ], - ), - ); - } - - void _showSelesaikanDialog(BuildContext context, dynamic item) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Selesaikan Pengaduan'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'), - const SizedBox(height: 16), - const Text( - 'Apakah Anda yakin ingin menyelesaikan pengaduan ini?', - textAlign: TextAlign.center, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Batal'), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context); - controller.selesaikanPengaduan(item.id!); - }, - child: const Text('Selesaikan'), - ), - ], - ), - ); - } -} diff --git a/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart index 3ea5ffc..320249c 100644 --- a/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart @@ -34,6 +34,13 @@ class TambahPenyaluranView extends GetView { final Rx selectedSkemaBantuan = Rx(null); final RxInt jumlahPenerima = 0.obs; + final RxDouble jumlahDiterimaPerOrang = 0.0.obs; + final RxString namaStokBantuan = ''.obs; + final RxString satuanStokBantuan = ''.obs; + final RxDouble totalStokDibutuhkan = 0.0.obs; + final RxDouble totalStokTersedia = 0.0.obs; + final RxBool isStokCukup = false.obs; + final RxBool isUang = false.obs; // Tanggal dan waktu penyaluran final Rx selectedDate = Rx(null); @@ -50,11 +57,72 @@ class TambahPenyaluranView extends GetView { print('pengajuan $pengajuanData'); jumlahPenerima.value = pengajuanData.length; + + // Hitung total stok yang dibutuhkan + totalStokDibutuhkan.value = + jumlahPenerima.value * jumlahDiterimaPerOrang.value; + + // Perbarui status kecukupan stok + isStokCukup.value = + totalStokTersedia.value >= totalStokDibutuhkan.value; } catch (e) { print('Error loading pengajuan kelayakan: $e'); } } + // Fungsi untuk memuat informasi stok bantuan + Future loadStokBantuanInfo(String stokBantuanId) async { + try { + if (stokBantuanId.isEmpty) { + namaStokBantuan.value = 'Tidak ada stok terkait'; + satuanStokBantuan.value = ''; + totalStokTersedia.value = 0; + isStokCukup.value = false; + isUang.value = false; + return; + } + + final stokData = await controller.supabaseService.client + .from('stok_bantuan') + .select('*') + .eq('id', stokBantuanId) + .single(); + + print('stokData $stokData'); + + if (stokData != null) { + namaStokBantuan.value = + stokData['nama'] ?? 'Nama stok tidak tersedia'; + satuanStokBantuan.value = stokData['satuan'] ?? 'Tidak ada satuan'; + isUang.value = stokData['is_uang'] ?? false; + + // Ambil jumlah stok tersedia + if (stokData['total_stok'] != null) { + totalStokTersedia.value = stokData['total_stok'].toDouble(); + } else { + totalStokTersedia.value = 0; + } + + // Periksa kecukupan stok + isStokCukup.value = + totalStokTersedia.value >= totalStokDibutuhkan.value; + } else { + namaStokBantuan.value = 'Stok tidak ditemukan'; + satuanStokBantuan.value = ''; + totalStokTersedia.value = 0; + isStokCukup.value = false; + isUang.value = false; + } + } catch (e) { + print('Error loading stok bantuan: $e'); + namaStokBantuan.value = 'Error memuat data stok'; + satuanStokBantuan.value = ''; + totalStokTersedia.value = 0; + isStokCukup.value = false; + isUang.value = false; + } + } + return Padding( padding: const EdgeInsets.all(16.0), child: Form( @@ -72,33 +140,6 @@ class TambahPenyaluranView extends GetView { ), const SizedBox(height: 24), - // Nama Penyaluran - Text( - 'Nama Penyaluran', - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - TextFormField( - controller: namaController, - decoration: InputDecoration( - hintText: 'Masukkan nama penyaluran', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Nama penyaluran tidak boleh kosong'; - } - return null; - }, - ), - const SizedBox(height: 16), - // Skema Bantuan Text( 'Skema Bantuan', @@ -128,6 +169,20 @@ class TambahPenyaluranView extends GetView { if (value != null) { selectedSkemaBantuan.value = controller.skemaBantuanCache[value]; + + // Set jumlah yang diterima per orang + jumlahDiterimaPerOrang.value = selectedSkemaBantuan + .value?.jumlahDiterimaPerOrang ?? + 0.0; + + // Load stok bantuan info + if (selectedSkemaBantuan.value?.stokBantuanId != null) { + await loadStokBantuanInfo( + selectedSkemaBantuan.value!.stokBantuanId!); + } else { + namaStokBantuan.value = 'Tidak ada stok terkait'; + } + await loadPengajuanKelayakan(value); } }, @@ -139,6 +194,405 @@ class TambahPenyaluranView extends GetView { }, )), const SizedBox(height: 16), + // Jumlah Penerima (Otomatis) + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Jumlah Penerima', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(width: 4), + Tooltip( + message: + 'Jumlah penerima dari pengajuan kelayakan yang terverifikasi.', + triggerMode: TooltipTriggerMode.tap, + child: Icon( + Icons.info_outline, + size: 16, + color: Colors.grey[600], + ), + ), + ], + ), + const SizedBox(height: 8), + Obx(() => TextFormField( + readOnly: true, + controller: TextEditingController( + text: jumlahPenerima.value.toString()), + decoration: InputDecoration( + hintText: + 'Jumlah penerima akan diambil otomatis', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + )), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + children: [ + // Jumlah Diterima Per Orang (dari skema) + Row( + children: [ + Text( + 'Jumlah Per Penerima', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(width: 4), + Tooltip( + message: + 'Jumlah yang akan diterima setiap penerima bantuan.', + triggerMode: TooltipTriggerMode.tap, + child: Icon( + Icons.info_outline, + size: 16, + color: Colors.grey[600], + ), + ), + ], + ), + const SizedBox(height: 8), + Obx(() => TextFormField( + readOnly: true, + controller: TextEditingController( + text: + jumlahDiterimaPerOrang.value.toString()), + decoration: InputDecoration( + hintText: + 'Jumlah diterima per orang dari skema bantuan', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + suffixText: satuanStokBantuan.value.isNotEmpty + ? satuanStokBantuan.value + : 'satuan', + ), + )), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Daftar Penerima', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 4), + Obx(() => OutlinedButton.icon( + onPressed: jumlahPenerima.value > 0 + ? () async { + final pengajuanData = await controller + .supabaseService.client + .from('xx02_pengajuan_kelayakan_bantuan') + .select('*, warga:warga_id(*)') + .eq('skema_bantuan_id', + selectedSkemaBantuanId.value ?? '') + .eq('status', 'TERVERIFIKASI'); + + Get.dialog( + Dialog( + child: Container( + width: + MediaQuery.of(context).size.width * 0.9, + height: + MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Daftar Penerima Bantuan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SingleChildScrollView( + child: DataTable( + columnSpacing: 20, + horizontalMargin: 20, + columns: const [ + DataColumn(label: Text('No')), + DataColumn(label: Text('Nama')), + DataColumn(label: Text('NIK')), + DataColumn( + label: Text('Alamat')), + ], + rows: pengajuanData + .asMap() + .entries + .map((entry) { + final warga = + entry.value['warga']; + return DataRow( + cells: [ + DataCell(Text( + '${entry.key + 1}')), + DataCell(Text( + warga['nama_lengkap'] ?? + '-')), + DataCell(Text( + warga['nik'] ?? '-')), + DataCell(Text( + warga['alamat'] ?? + '-')), + ], + ); + }).toList(), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + : null, + icon: const Icon(Icons.people), + label: const Text('Lihat Daftar'), + style: ButtonStyle( + foregroundColor: + WidgetStateProperty.resolveWith((states) { + return jumlahPenerima.value <= 0 + ? Colors.grey + : Theme.of(context).primaryColor; + }), + backgroundColor: + WidgetStateProperty.resolveWith((states) { + return jumlahPenerima.value <= 0 + ? Colors.grey.withOpacity(0.1) + : Theme.of(context).primaryColor.withOpacity(0.1); + }), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: jumlahPenerima.value <= 0 + ? Colors.grey.withOpacity(0.5) + : Theme.of(context).primaryColor, + ), + ), + ), + ), + )), + + const SizedBox(height: 16), + + // Informasi Stok Bantuan + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber[50], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber[200]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Informasi Stok Bantuan', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.amber[800], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Nama Stok'), + const SizedBox(height: 4), + Obx(() => Text( + namaStokBantuan.value, + style: const TextStyle( + fontWeight: FontWeight.bold), + )), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Satuan Stok'), + const SizedBox(height: 4), + Obx(() => Text( + satuanStokBantuan.value, + style: const TextStyle( + fontWeight: FontWeight.bold), + )), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Total Stok Tersedia'), + const SizedBox(height: 4), + Obx(() => Text( + isUang.value + ? 'Rp ${DateTimeHelper.formatNumber(totalStokTersedia.value)}' + : '${totalStokTersedia.value} ${satuanStokBantuan.value}', + style: const TextStyle( + fontWeight: FontWeight.bold), + )), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('Total Stok Dibutuhkan'), + const SizedBox(width: 4), + Tooltip( + message: + 'Total stok yang dibutuhkan dihitung dari jumlah penerima × jumlah yang diterima per orang', + triggerMode: TooltipTriggerMode.tap, + child: const Icon(Icons.info_outline, + size: 16), + ), + ], + ), + const SizedBox(height: 4), + Obx(() => Text( + isUang.value + ? 'Rp ${DateTimeHelper.formatNumber(totalStokDibutuhkan.value)}' + : '${totalStokDibutuhkan.value} ${satuanStokBantuan.value}', + style: const TextStyle( + fontWeight: FontWeight.bold), + )), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Obx(() => selectedSkemaBantuanId.value != null + ? Container( + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 12), + decoration: BoxDecoration( + color: isStokCukup.value + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: isStokCukup.value + ? Colors.green + : Colors.red, + ), + ), + child: Row( + children: [ + Icon( + isStokCukup.value + ? Icons.check_circle + : Icons.error, + color: isStokCukup.value + ? Colors.green + : Colors.red, + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + isStokCukup.value + ? 'Stok tersedia cukup untuk penyaluran' + : 'Stok tidak cukup untuk penyaluran! Tambah stok terlebih dahulu.', + style: TextStyle( + color: isStokCukup.value + ? Colors.green[800] + : Colors.red[800], + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + ) + : const SizedBox()), + ], + ), + ), + const SizedBox(height: 16), + // Nama Penyaluran + Text( + 'Judul Penyaluran', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + TextFormField( + controller: namaController, + decoration: InputDecoration( + hintText: 'Masukkan judul penyaluran', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Judul penyaluran tidak boleh kosong'; + } + return null; + }, + ), + const SizedBox(height: 16), // Lokasi Penyaluran Text( @@ -176,128 +630,25 @@ class TambahPenyaluranView extends GetView { )), const SizedBox(height: 16), - // Jumlah Penerima (Otomatis) - Text( - 'Jumlah Penerima', - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - Obx(() => TextFormField( - readOnly: true, - controller: TextEditingController( - text: jumlahPenerima.value.toString()), - decoration: InputDecoration( - hintText: 'Jumlah penerima akan diambil otomatis', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ), - )), - const SizedBox(height: 8), - Text( - 'Info: Jumlah penerima diambil dari data pengajuan kelayakan bantuan yang telah terverifikasi untuk skema bantuan yang dipilih.', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - const SizedBox(height: 8), - Obx(() => jumlahPenerima.value > 0 - ? TextButton.icon( - onPressed: () async { - final pengajuanData = await controller - .supabaseService.client - .from('xx02_pengajuan_kelayakan_bantuan') - .select('*, warga:warga_id(*)') - .eq('skema_bantuan_id', - selectedSkemaBantuanId.value ?? '') - .eq('status', 'TERVERIFIKASI'); - - Get.dialog( - Dialog( - child: Container( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height * 0.8, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Daftar Penerima Bantuan', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - onPressed: () => Get.back(), - icon: const Icon(Icons.close), - ), - ], - ), - const SizedBox(height: 16), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - child: DataTable( - columnSpacing: 20, - horizontalMargin: 20, - columns: const [ - DataColumn(label: Text('No')), - DataColumn(label: Text('Nama')), - DataColumn(label: Text('NIK')), - DataColumn(label: Text('Alamat')), - ], - rows: pengajuanData - .asMap() - .entries - .map((entry) { - final warga = entry.value['warga']; - return DataRow( - cells: [ - DataCell( - Text('${entry.key + 1}')), - DataCell(Text( - warga['nama_lengkap'] ?? - '-')), - DataCell( - Text(warga['nik'] ?? '-')), - DataCell(Text( - warga['alamat'] ?? '-')), - ], - ); - }).toList(), - ), - ), - ), - ), - ], - ), - ), - ), - ); - }, - icon: const Icon(Icons.people), - label: const Text('Lihat Daftar Penerima'), - ) - : const SizedBox.shrink()), - const SizedBox(height: 8), - // Tanggal Penyaluran - Text( - 'Tanggal Penyaluran', - style: Theme.of(context).textTheme.titleSmall, + Row( + children: [ + Text( + 'Tanggal Penyaluran', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(width: 4), + Tooltip( + message: + 'Tanggal pelaksanaan minimal 1 hari sebelum dijadwalkan', + triggerMode: TooltipTriggerMode.tap, + child: Icon( + Icons.info_outline, + size: 16, + color: Colors.grey[600], + ), + ), + ], ), const SizedBox(height: 8), TextFormField( @@ -337,36 +688,7 @@ class TambahPenyaluranView extends GetView { return null; }, ), - const SizedBox(height: 10), - // Hint info tanggal minimal - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue[50], - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue[200]!), - ), - child: Row( - children: [ - const Icon( - Icons.info_outline, - color: Colors.blue, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Tanggal pelaksanaan minimal 1 hari sebelum dijadwalkan', - style: TextStyle( - color: Colors.blue[900], - fontSize: 12, - ), - ), - ), - ], - ), - ), const SizedBox(height: 16), // Waktu Mulai @@ -445,6 +767,19 @@ class TambahPenyaluranView extends GetView { child: ElevatedButton( onPressed: () { if (formKey.currentState!.validate()) { + // Periksa kecukupan stok + if (!isStokCukup.value) { + Get.snackbar( + 'Stok Tidak Cukup', + 'Stok bantuan tidak mencukupi untuk penyaluran ini. Silakan tambah stok terlebih dahulu.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 4), + ); + return; + } + // Gabungkan tanggal dan waktu mulai DateTime? tanggalWaktuMulai; if (selectedDate.value != null && @@ -460,15 +795,18 @@ class TambahPenyaluranView extends GetView { // Panggil fungsi untuk menambahkan penyaluran controller.tambahPenyaluran( - nama: namaController.text, - deskripsi: deskripsiController.text, - skemaId: selectedSkemaBantuanId.value!, - lokasiPenyaluranId: selectedLokasiPenyaluranId.value!, - jumlahPenerima: jumlahPenerima.value, - tanggalPenyaluran: tanggalWaktuMulai, - kategoriBantuanId: - selectedSkemaBantuan.value!.kategoriBantuanId!, - ); + nama: namaController.text, + deskripsi: deskripsiController.text, + skemaId: selectedSkemaBantuanId.value!, + lokasiPenyaluranId: selectedLokasiPenyaluranId.value!, + jumlahPenerima: jumlahPenerima.value, + tanggalPenyaluran: tanggalWaktuMulai, + kategoriBantuanId: + selectedSkemaBantuan.value!.kategoriBantuanId!, + jumlahDiterimaPerOrang: jumlahDiterimaPerOrang.value, + stokBantuanId: + selectedSkemaBantuan.value!.stokBantuanId!, + totalStokDibutuhkan: totalStokDibutuhkan.value); } }, style: ElevatedButton.styleFrom( diff --git a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart index 161d022..91f899f 100644 --- a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart +++ b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart @@ -110,33 +110,37 @@ class WargaDashboardController extends GetxController { ), penyaluran_bantuan:penyaluran_bantuan_id( *, - lokasi_penyaluran(*) + lokasi_penyaluran(*), + kategori_bantuan(*) ) ''').eq('warga_id', wargaId).order('created_at', ascending: false); final List penerima = []; + + // Loop melalui setiap data penerima for (var item in response) { + // Pastikan data penerima sesuai dengan tipe data yang diharapkan Map sanitizedPenerimaData = Map.from(item); + // Konversi jumlah_bantuan ke double jika bertipe String if (sanitizedPenerimaData['jumlah_bantuan'] is String) { - var jumlahBantuan = double.tryParse( - sanitizedPenerimaData['jumlah_bantuan'] as String); - sanitizedPenerimaData['jumlah_bantuan'] = jumlahBantuan; + sanitizedPenerimaData['jumlah_bantuan'] = + double.tryParse(sanitizedPenerimaData['jumlah_bantuan']) ?? 0.0; } - // Tambahkan informasi apakah bantuan uang atau bukan dan satuan + // Ambil data dari stok bantuan jika tersedia if (sanitizedPenerimaData['stok_bantuan'] != null) { - // Cek apakah bantuan uang + // Cek apakah bantuan berupa uang atau barang final isUang = sanitizedPenerimaData['stok_bantuan']['is_uang'] ?? false; sanitizedPenerimaData['is_uang'] = isUang; - // Ambil satuan + // Ambil satuan bantuan final satuan = sanitizedPenerimaData['stok_bantuan']['satuan'] ?? ''; sanitizedPenerimaData['satuan'] = satuan; - // Ambil nama kategori bantuan jika tersedia + // Ambil nama kategori bantuan if (sanitizedPenerimaData['stok_bantuan']['kategori_bantuan'] != null) { final kategoriNama = sanitizedPenerimaData['stok_bantuan'] @@ -146,7 +150,7 @@ class WargaDashboardController extends GetxController { } } - // Tambahkan informasi dari penyaluran bantuan + // Ambil data dari penyaluran bantuan jika tersedia if (sanitizedPenerimaData['penyaluran_bantuan'] != null) { // Ambil nama penyaluran final namaPenyaluran = @@ -158,6 +162,11 @@ class WargaDashboardController extends GetxController { sanitizedPenerimaData['penyaluran_bantuan']['deskripsi'] ?? ''; sanitizedPenerimaData['deskripsi_penyaluran'] = deskripsiPenyaluran; + // Ambil status penyaluran + final statusPenyaluran = + sanitizedPenerimaData['penyaluran_bantuan']['status'] ?? ''; + sanitizedPenerimaData['status_penyaluran'] = statusPenyaluran; + // Ambil lokasi penyaluran jika tersedia if (sanitizedPenerimaData['penyaluran_bantuan'] ['lokasi_penyaluran'] != @@ -172,6 +181,19 @@ class WargaDashboardController extends GetxController { ''; sanitizedPenerimaData['lokasi_penyaluran_alamat'] = lokasiAlamat; } + + // Ambil kategori bantuan dari relasi langsung jika ada + if (sanitizedPenerimaData['penyaluran_bantuan']['kategori_bantuan'] != + null) { + final kategoriNama = sanitizedPenerimaData['penyaluran_bantuan'] + ['kategori_bantuan']['nama'] ?? + ''; + // Jika belum ada kategori_nama dari stok_bantuan, gunakan dari relasi langsung + if (sanitizedPenerimaData['kategori_nama'] == null || + sanitizedPenerimaData['kategori_nama'].isEmpty) { + sanitizedPenerimaData['kategori_nama'] = kategoriNama; + } + } } var model = PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData); @@ -396,4 +418,74 @@ class WargaDashboardController extends GetxController { ); } } + + // Metode untuk menambahkan pengaduan baru + Future addPengaduan({ + required String judul, + required String deskripsi, + required String penerimaPenyaluranId, + List fotoPengaduanPaths = const [], + }) async { + try { + isLoading.value = true; + + // Cari warga_id berdasarkan user_id + final wargaData = await _supabaseService.getWargaByUserId(); + if (wargaData == null) { + throw Exception('Data warga tidak ditemukan'); + } + + final String wargaId = wargaData['id']; + + // Upload foto pengaduan jika ada + List fotoPengaduanUrls = []; + if (fotoPengaduanPaths.isNotEmpty) { + fotoPengaduanUrls = await _supabaseService.uploadMultipleFiles( + fotoPengaduanPaths, 'pengaduan', 'foto_pengaduan') ?? + []; + } + + // Buat objek pengaduan + final Map pengaduanData = { + 'judul': judul, + 'deskripsi': deskripsi, + 'status': 'MENUNGGU', + 'warga_id': wargaId, + 'penerima_penyaluran_id': penerimaPenyaluranId, + 'foto_pengaduan': fotoPengaduanUrls, + 'tanggal_pengaduan': DateTime.now().toIso8601String(), + 'created_at': DateTime.now().toIso8601String(), + 'updated_at': DateTime.now().toIso8601String(), + }; + + // Simpan pengaduan ke Supabase + await _supabaseService.client.from('pengaduan').insert(pengaduanData); + + // Refresh data pengaduan + await fetchPengaduan(); + + Get.snackbar( + 'Berhasil', + 'Pengaduan berhasil dibuat dan akan segera diproses', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + + return true; + } catch (e) { + print('Error membuat pengaduan: $e'); + Get.snackbar( + 'Error', + 'Gagal membuat pengaduan: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return false; + } finally { + isLoading.value = false; + } + } } diff --git a/lib/app/modules/warga/views/detail_pengaduan_view.dart b/lib/app/modules/warga/views/detail_pengaduan_view.dart index 3fb700e..c5a4a62 100644 --- a/lib/app/modules/warga/views/detail_pengaduan_view.dart +++ b/lib/app/modules/warga/views/detail_pengaduan_view.dart @@ -916,36 +916,12 @@ class WargaDetailPengaduanView extends GetView { ), ], ), - - // Prioritas tindakan (jika ada) - if (tindakan.prioritas != null) ...[ - const SizedBox(height: 8), - // Menggunakan StatusPill untuk prioritas tindakan - StatusPill( - status: tindakan.prioritasText, - backgroundColor: _getPriorityColor(tindakan.prioritas), - textColor: Colors.white, - ), - ], ], ), ), ); } - Color _getPriorityColor(String? priority) { - switch (priority) { - case 'TINGGI': - return Colors.red; - case 'SEDANG': - return Colors.orange; - case 'RENDAH': - return Colors.green; - default: - return Colors.grey; - } - } - void showFullScreenImage(BuildContext context, String imageUrl) { // Buat controller untuk InteractiveViewer final TransformationController transformationController = diff --git a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart index 5f99b89..981a3ec 100644 --- a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart +++ b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; +import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; class WargaDetailPenerimaanView extends GetView { const WargaDetailPenerimaanView({super.key}); @@ -17,7 +20,14 @@ class WargaDetailPenerimaanView extends GetView { // Segera muat ulang data penerimaan ketika halaman dibuka WidgetsBinding.instance.addPostFrameCallback((_) { + // Pastikan tidak ada dialog yang terbuka terlebih dahulu + if (Get.isDialogOpen == true) { + Get.back(); + } + + // Kemudian muat data baru controller.fetchPenerimaPenyaluran(); + controller.fetchPengaduan(); // Tambahkan untuk memuat data pengaduan }); if (id == null) { @@ -53,6 +63,11 @@ class WargaDetailPenerimaanView extends GetView { final bool isDiterima = penyaluran.statusPenerimaan == 'DITERIMA'; + // Cek apakah ada pengaduan untuk penyaluran ini + final pengaduan = controller.pengaduan.firstWhereOrNull( + (item) => item.penerimaPenyaluranId == penyaluranId); + final bool hasPengaduan = pengaduan != null; + return Scaffold( appBar: AppBar( title: const Text('Detail Penerimaan'), @@ -82,6 +97,19 @@ class WargaDetailPenerimaanView extends GetView { children: [ _buildHeaderSection(penyaluran), const SizedBox(height: 16), + + // Tampilkan pengaduan yang ada (jika ada) di bagian atas + if (hasPengaduan) ...[ + _buildExistingPengaduanSection(pengaduan), + const SizedBox(height: 16), + ], + + if (penyaluran.qrCodeHash != null && + penyaluran.qrCodeHash!.isNotEmpty) + _buildQrCodeSection(penyaluran), + if (penyaluran.qrCodeHash != null && + penyaluran.qrCodeHash!.isNotEmpty) + const SizedBox(height: 16), _buildDetailSection(penyaluran), const SizedBox(height: 16), _buildLocationSection(penyaluran), @@ -89,6 +117,12 @@ class WargaDetailPenerimaanView extends GetView { if (isDiterima) _buildBuktiPenerimaanSection(penyaluran), if (isDiterima) const SizedBox(height: 16), _buildAdditionalInfoSection(penyaluran), + + // Tampilkan section pengaduan jika belum ada pengaduan + if (isDiterima && !hasPengaduan) ...[ + const SizedBox(height: 16), + _buildPengaduanSection(penyaluran), + ], ], ), ), @@ -123,10 +157,10 @@ class WargaDetailPenerimaanView extends GetView { borderRadius: BorderRadius.circular(16), ), child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - gradient: AppTheme.primaryGradient, - ), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(16), + // gradient: AppTheme.primaryGradient, + // ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -142,18 +176,10 @@ class WargaDetailPenerimaanView extends GetView { style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: Colors.white, + color: Colors.black, ), ), ), - StatusBadge( - status: penyaluran.statusPenerimaan ?? 'MENUNGGU', - fontSize: 14, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - ), ], ), const SizedBox(height: 16), @@ -163,8 +189,8 @@ class WargaDetailPenerimaanView extends GetView { padding: const EdgeInsets.only(bottom: 16), child: Text( penyaluran.deskripsiPenyaluran!, - style: TextStyle( - color: Colors.white, + style: const TextStyle( + color: Colors.black, fontSize: 16, ), ), @@ -232,6 +258,94 @@ class WargaDetailPenerimaanView extends GetView { ], ), ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Status Bantuan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Status Penyaluran', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + StatusBadge( + status: penyaluran.statusPenyaluran ?? '', + fontSize: 14, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Status Penerimaan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + StatusBadge( + status: + penyaluran.statusPenerimaan ?? 'MENUNGGU', + fontSize: 14, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), ], ), ), @@ -270,6 +384,7 @@ class WargaDetailPenerimaanView extends GetView { icon: Icons.category, title: 'Kategori', value: penyaluran.kategoriNama ?? 'Tidak tersedia', + statusColor: null, ), const Divider(height: 24), _buildDetailItem( @@ -279,6 +394,7 @@ class WargaDetailPenerimaanView extends GetView { ? DateFormat('dd MMMM yyyy', 'id_ID') .format(penyaluran.tanggalPenerimaan!) : 'Belum diterima', + statusColor: null, ), const Divider(height: 24), _buildDetailItem( @@ -288,7 +404,63 @@ class WargaDetailPenerimaanView extends GetView { ? DateFormat('HH:mm', 'id_ID') .format(penyaluran.tanggalPenerimaan!) : 'Belum diterima', + statusColor: null, ), + const Divider(height: 24), + Row( + children: [ + Icon( + Icons.assignment_turned_in, + color: Get.theme.primaryColor, + ), + const SizedBox(width: 8), + const Text( + 'Status Penerimaan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + StatusBadge( + status: penyaluran.statusPenerimaan ?? 'MENUNGGU', + fontSize: 12, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + ), + ], + ), + if (penyaluran.statusPenyaluran != null && + penyaluran.statusPenyaluran!.isNotEmpty) ...[ + const Divider(height: 24), + Row( + children: [ + Icon( + Icons.check_circle, + color: Get.theme.primaryColor, + ), + const SizedBox(width: 8), + const Text( + 'Status Penyaluran', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + StatusBadge( + status: penyaluran.statusPenyaluran!, + fontSize: 12, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + ), + ], + ), + ], if (penyaluran.keterangan != null && penyaluran.keterangan!.isNotEmpty) ...[ const Divider(height: 24), @@ -296,6 +468,7 @@ class WargaDetailPenerimaanView extends GetView { icon: Icons.note, title: 'Keterangan', value: penyaluran.keterangan!, + statusColor: null, ), ], ], @@ -336,6 +509,7 @@ class WargaDetailPenerimaanView extends GetView { icon: Icons.place, title: 'Tempat Penerimaan', value: penyaluran.lokasiPenyaluranNama ?? 'Tidak tersedia', + statusColor: null, ), if (penyaluran.lokasiPenyaluranAlamat != null && penyaluran.lokasiPenyaluranAlamat!.isNotEmpty) ...[ @@ -344,6 +518,7 @@ class WargaDetailPenerimaanView extends GetView { icon: Icons.map, title: 'Alamat', value: penyaluran.lokasiPenyaluranAlamat!, + statusColor: null, ), ], const SizedBox(height: 16), @@ -570,12 +745,14 @@ class WargaDetailPenerimaanView extends GetView { icon: Icons.numbers, title: 'ID Penerimaan', value: '${penyaluran.id}', + statusColor: null, ), const Divider(height: 24), _buildDetailItem( icon: Icons.person, title: 'Penerima', value: controller.nama, + statusColor: null, ), const Divider(height: 24), _buildDetailItem( @@ -585,94 +762,839 @@ class WargaDetailPenerimaanView extends GetView { ? DateFormat('dd MMMM yyyy HH:mm', 'id_ID') .format(penyaluran.tanggalPenerimaan!) : 'Tidak tersedia', + statusColor: null, ), - - // Tambahkan QR Code untuk verifikasi - if (penyaluran.qrCodeHash != null && - penyaluran.qrCodeHash!.isNotEmpty) ...[ - const Divider(height: 24), - _buildQrCodeSection(penyaluran), - ], ], ), ), ); } - // Tambahkan widget untuk menampilkan QR Code Widget _buildQrCodeSection(PenerimaPenyaluranModel penyaluran) { // Pastikan menggunakan data terbaru dari model dan cetak ke log untuk debugging final qrData = penyaluran.qrCodeHash ?? 'invalid-qr-code'; - print('QR Code Hash: $qrData'); // Log untuk debugging + print('penyaluran.statusPenyaluran ${penyaluran.statusPenyaluran}'); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + // Cek status penyaluran untuk disabled state + final bool isDisabled = penyaluran.statusPenyaluran != null && + (penyaluran.statusPenyaluran!.toUpperCase() == 'DIJADWALKAN' || + penyaluran.statusPenyaluran!.toUpperCase() == 'DISETUJUI' || + penyaluran.statusPenyaluran!.toUpperCase() == 'BATALTERLAKSANA' || + penyaluran.statusPenyaluran!.toUpperCase() == 'TERLAKSANA'); + + final String statusMessage; + if (isDisabled) { + if (penyaluran.statusPenyaluran!.toUpperCase() == 'BATALTERLAKSANA') { + statusMessage = + 'QR Code tidak dapat digunakan karena penyaluran dibatalkan'; + } else if (penyaluran.statusPenyaluran!.toUpperCase() == 'TERLAKSANA') { + statusMessage = + 'QR Code sudah digunakan pada penyaluran yang telah terlaksana'; + } else { + statusMessage = + 'QR Code belum dapat digunakan karena penyaluran belum terlaksana'; + } + } else { + statusMessage = + 'Tunjukkan QR Code ini kepada petugas untuk verifikasi penerimaan'; + } + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.qr_code, - color: Get.theme.primaryColor, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.qr_code, + color: isDisabled ? Colors.grey : Get.theme.primaryColor, + ), + const SizedBox(width: 8), + const Text( + 'QR Code Verifikasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (isDisabled) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.withOpacity(0.5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.lock, + size: 14, + color: Colors.black.withOpacity(0.5), + ), + const SizedBox(width: 4), + Text( + 'Tidak Aktif', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], ), - const SizedBox(width: 8), - const Text( - 'QR Code Verifikasi', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + const SizedBox(height: 16), + Center( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + // QR Code + ColorFiltered( + colorFilter: ColorFilter.mode( + isDisabled + ? Colors.grey.withOpacity(0.5) + : Colors.transparent, + BlendMode.srcATop, + ), + child: QrImageView( + key: UniqueKey(), + data: qrData, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, + errorStateBuilder: (cxt, err) { + return const Center( + child: Text( + "QR Code tidak tersedia", + textAlign: TextAlign.center, + ), + ); + }, + ), + ), + + // Overlay disabled jika perlu + if (isDisabled) + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.grey.withOpacity(0.2), + ), + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'TIDAK AKTIF', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + statusMessage, + style: TextStyle( + fontSize: 14, + color: isDisabled + ? Colors.grey.shade700 + : Colors.grey.shade700, + fontWeight: + isDisabled ? FontWeight.bold : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + ], + ), ), ), ], ), - const SizedBox(height: 16), - Center( - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - blurRadius: 4, - spreadRadius: 1, - offset: const Offset(0, 2), + ), + ); + } + + Widget _buildPengaduanSection(PenerimaPenyaluranModel penyaluran) { + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.report_problem, + color: Colors.orange.shade700, + ), + const SizedBox(width: 8), + const Text( + 'Pengaduan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), - child: Column( + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Apakah ada masalah dengan penyaluran ini?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.orange.shade900, + ), + ), + const SizedBox(height: 8), + Text( + 'Silakan laporkan jika ada masalah atau keluhan terkait penyaluran bantuan ini.', + style: TextStyle( + fontSize: 14, + color: Colors.orange.shade800, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => _showPengaduanDialog(penyaluran), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.add), + label: const Text('Buat Pengaduan'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + void _showPengaduanDialog(PenerimaPenyaluranModel penyaluran) { + final TextEditingController judulController = TextEditingController(); + final TextEditingController deskripsiController = TextEditingController(); + final RxList fotoPengaduanPaths = [].obs; + final ImagePicker imagePicker = ImagePicker(); + + // Fungsi untuk mengambil foto pengaduan + Future pickFotoPengaduan(ImageSource source) async { + try { + final pickedFile = await imagePicker.pickImage( + source: source, + imageQuality: 70, + maxWidth: 1000, + ); + + if (pickedFile != null) { + fotoPengaduanPaths.add(pickedFile.path); + } + } catch (e) { + print('Error picking image: $e'); + Get.snackbar( + 'Error', + 'Gagal mengambil gambar: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Fungsi untuk menampilkan dialog pilih sumber foto + void showPilihSumberFoto() { + Get.bottomSheet( + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Pilih Sumber Foto', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text('Kamera'), + onTap: () { + Get.back(); + pickFotoPengaduan(ImageSource.camera); + }, + ), + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text('Galeri'), + onTap: () { + Get.back(); + pickFotoPengaduan(ImageSource.gallery); + }, + ), + ], + ), + ), + ); + } + + // Fungsi untuk menghapus foto dari daftar + void removeFoto(int index) { + if (index >= 0 && index < fotoPengaduanPaths.length) { + fotoPengaduanPaths.removeAt(index); + } + } + + // Fungsi untuk memeriksa dan menutup loading dialog + void closeLoadingDialog() { + // Periksa apakah ada dialog yang terbuka sebelum menutupnya + if (Get.isDialogOpen == true) { + Get.back(); + } + } + + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon( + Icons.report_problem, + color: Colors.orange.shade700, + ), + const SizedBox(width: 8), + const Text('Buat Pengaduan'), + ], + ), + content: Obx(() => SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Detail Penyaluran:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Text('ID: ${penyaluran.id}'), + Text( + 'Nama Penyaluran: ${penyaluran.namaPenyaluran ?? 'Tidak tersedia'}'), + Text( + 'Status: ${penyaluran.statusPenyaluran ?? 'Tidak tersedia'}'), + const SizedBox(height: 16), + TextField( + controller: judulController, + decoration: const InputDecoration( + labelText: 'Judul Pengaduan', + hintText: 'Masukkan judul pengaduan', + border: OutlineInputBorder(), + ), + maxLines: 1, + ), + const SizedBox(height: 16), + TextField( + controller: deskripsiController, + decoration: const InputDecoration( + labelText: 'Deskripsi Pengaduan', + hintText: 'Jelaskan masalah atau keluhan Anda', + border: OutlineInputBorder(), + ), + maxLines: 5, + ), + const SizedBox(height: 16), + + // Widget untuk mengunggah foto pengaduan + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Foto Pengaduan:', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ElevatedButton.icon( + onPressed: showPilihSumberFoto, + icon: const Icon(Icons.add_a_photo), + label: const Text('Tambah Foto'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + + // Tampilkan foto yang sudah dipilih + if (fotoPengaduanPaths.isNotEmpty) + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: fotoPengaduanPaths.length, + itemBuilder: (context, index) { + return Stack( + children: [ + Container( + width: 100, + height: 100, + margin: const EdgeInsets.only( + right: 8, + top: 8, + bottom: 8, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: FileImage( + File(fotoPengaduanPaths[index]), + ), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => removeFoto(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ); + }, + ), + ) + else + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: const Text( + 'Belum ada foto yang dipilih', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + ), + ), + ), + ], + ), + ], + ), + )), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () async { + if (judulController.text.isEmpty || + deskripsiController.text.isEmpty) { + Get.snackbar( + 'Error', + 'Judul dan deskripsi pengaduan harus diisi', + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + Get.back(); // Tutup dialog terlebih dahulu + + // Tampilkan loading + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + bool success = false; + try { + // Simpan pengaduan ke database dengan foto + success = await controller.addPengaduan( + judul: judulController.text, + deskripsi: deskripsiController.text, + penerimaPenyaluranId: penyaluran.id!, + fotoPengaduanPaths: fotoPengaduanPaths.toList(), + ); + } catch (e) { + print('Error saat membuat pengaduan: $e'); + Get.snackbar( + 'Error', + 'Terjadi kesalahan saat membuat pengaduan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + // Pastikan dialog loading ditutup dalam kondisi apapun + closeLoadingDialog(); + } + + if (success) { + // Refresh data halaman + await controller.fetchPengaduan(); + await controller.fetchPenerimaPenyaluran(); + + Get.snackbar( + 'Sukses', + 'Pengaduan berhasil dikirim dan data diperbarui', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + + // Navigate ke halaman pengaduan jika berhasil + controller.changeTab(2); // Tab pengaduan + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + ), + child: const Text('Kirim'), + ), + ], + ), + ); + } + + Widget _buildExistingPengaduanSection(PengaduanModel pengaduan) { + // Tentukan warna berdasarkan status pengaduan + Color statusColor; + String statusText; + + switch (pengaduan.status?.toUpperCase() ?? '') { + case 'MENUNGGU': + statusColor = Colors.orange; + statusText = 'Menunggu'; + break; + case 'TINDAKAN': + statusColor = Colors.blue; + statusText = 'Dalam Tindakan'; + break; + case 'SELESAI': + statusColor = Colors.green; + statusText = 'Selesai'; + break; + default: + statusColor = Colors.grey; + statusText = pengaduan.status ?? 'Tidak Diketahui'; + } + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: Colors.orange.shade50, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Gunakan UniqueKey() untuk memaksa rebuild widget QR code - QrImageView( - key: UniqueKey(), - data: qrData, - version: QrVersions.auto, - size: 200.0, - backgroundColor: Colors.white, - errorStateBuilder: (cxt, err) { - return const Center( - child: Text( - "QR Code tidak tersedia", - textAlign: TextAlign.center, + Row( + children: [ + Icon( + Icons.report_problem, + color: Colors.orange.shade700, + ), + const SizedBox(width: 8), + const Text( + 'Pengaduan Terdaftar', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: statusColor), + ), + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + pengaduan.judul ?? 'Pengaduan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + pengaduan.deskripsi ?? 'Tidak ada deskripsi', + style: const TextStyle(fontSize: 14), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 16), + + // Tampilkan foto pengaduan jika ada + if (pengaduan.fotoPengaduan != null && + pengaduan.fotoPengaduan!.isNotEmpty) ...[ + const Text( + 'Foto Pengaduan:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: pengaduan.fotoPengaduan!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + // Tampilkan gambar dalam ukuran penuh ketika diklik + Get.dialog( + Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ], + ), + ), + Image.network( + pengaduan.fotoPengaduan![index], + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.broken_image, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 8), + Text( + 'Gambar tidak dapat dimuat', + style: TextStyle( + color: Colors.grey.shade600), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ); + }, + child: Container( + width: 80, + height: 80, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + pengaduan.fotoPengaduan![index], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Icon( + Icons.broken_image, + color: Colors.grey.shade400, + ), + ); + }, + ), + ), ), ); }, ), - const SizedBox(height: 12), + ), + const SizedBox(height: 16), + ], + + // Tanggal pengaduan + Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 8), Text( - 'Tunjukkan QR Code ini kepada petugas untuk verifikasi penerimaan', + pengaduan.tanggalPengaduan != null + ? DateFormat('dd MMMM yyyy HH:mm', 'id_ID') + .format(pengaduan.tanggalPengaduan!) + : 'Tanggal tidak tersedia', style: TextStyle( - fontSize: 14, + fontSize: 12, color: Colors.grey.shade700, ), - textAlign: TextAlign.center, ), ], ), - ), + const SizedBox(height: 16), + + // Tombol lihat detail + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Get.toNamed('/warga/detail-pengaduan', + arguments: {'id': pengaduan.id}); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.remove_red_eye), + label: const Text('Lihat Detail Pengaduan'), + ), + ), + ], ), - ], + ), ); } @@ -680,6 +1602,7 @@ class WargaDetailPenerimaanView extends GetView { required IconData icon, required String title, required String value, + Color? statusColor, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -693,7 +1616,7 @@ class WargaDetailPenerimaanView extends GetView { child: Icon( icon, size: 20, - color: Colors.grey.shade700, + color: statusColor ?? Colors.grey.shade700, ), ), const SizedBox(width: 12), @@ -711,9 +1634,10 @@ class WargaDetailPenerimaanView extends GetView { const SizedBox(height: 4), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, + color: statusColor, ), ), ], diff --git a/lib/app/modules/warga/views/warga_penerimaan_view.dart b/lib/app/modules/warga/views/warga_penerimaan_view.dart index f5d33a2..aa17f63 100644 --- a/lib/app/modules/warga/views/warga_penerimaan_view.dart +++ b/lib/app/modules/warga/views/warga_penerimaan_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/widgets/bantuan_card.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; class WargaPenerimaanView extends GetView { const WargaPenerimaanView({super.key}); @@ -23,15 +24,6 @@ class WargaPenerimaanView extends GetView { : _buildPenerimaanList(), ); }), - floatingActionButton: FloatingActionButton( - onPressed: () { - // Navigasi ke halaman riwayat penerimaan - Get.toNamed('/riwayat-penyaluran'); - }, - backgroundColor: Colors.blue, - tooltip: 'Riwayat Penerimaan', - child: const Icon(Icons.history), - ), ); } @@ -43,13 +35,13 @@ class WargaPenerimaanView extends GetView { Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), + color: AppTheme.primaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.volunteer_activism, size: 80, - color: Colors.blue.shade400, + color: AppTheme.primaryColor, ), ), const SizedBox(height: 24), @@ -82,6 +74,8 @@ class WargaPenerimaanView extends GetView { label: const Text('Muat Ulang'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/lib/app/modules/warga/views/warga_pengaduan_view.dart b/lib/app/modules/warga/views/warga_pengaduan_view.dart index b2ed50c..f96ea4b 100644 --- a/lib/app/modules/warga/views/warga_pengaduan_view.dart +++ b/lib/app/modules/warga/views/warga_pengaduan_view.dart @@ -30,13 +30,13 @@ class WargaPengaduanView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.report_problem, + Icons.check_circle_outline, size: 80, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( - 'Belum Ada Pengaduan', + 'Bagus!', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -45,27 +45,12 @@ class WargaPengaduanView extends GetView { ), const SizedBox(height: 8), Text( - 'Anda belum membuat pengaduan', + 'Belum ada pengaduan yang dibuat', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () { - // TODO: Implementasi navigasi ke halaman buat pengaduan - Get.toNamed('/buat-pengaduan'); - }, - icon: const Icon(Icons.add), - label: const Text('Buat Pengaduan Baru'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ), ], ), ); diff --git a/lib/app/modules/warga/views/warga_view.dart b/lib/app/modules/warga/views/warga_view.dart index b7c2952..dd0db9f 100644 --- a/lib/app/modules/warga/views/warga_view.dart +++ b/lib/app/modules/warga/views/warga_view.dart @@ -108,16 +108,6 @@ class WargaView extends GetView { badgeColor: Colors.orange, onTap: () => controller.changeTab(2), ), - DrawerMenuItem( - icon: Icons.assignment_outlined, - title: 'Pengajuan Kelayakan', - onTap: () { - // TODO: Navigasi ke halaman pengajuan kelayakan - Get.toNamed('/pengajuan-kelayakan'); - }, - badgeCount: controller.totalPengajuanMenunggu.value, - badgeColor: Colors.blue, - ), ], )), body: Obx(() { @@ -161,20 +151,6 @@ class WargaView extends GetView { ), ], )), - floatingActionButton: Obx(() { - // Tampilkan FAB hanya di halaman pengaduan - if (controller.activeTabIndex.value == 2) { - return FloatingActionButton( - onPressed: () { - // TODO: Implementasi navigasi ke halaman buat pengaduan - Get.toNamed('/buat-pengaduan'); - }, - backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.add), - ); - } - return const SizedBox.shrink(); - }), ); } } diff --git a/lib/app/widgets/bantuan_card.dart b/lib/app/widgets/bantuan_card.dart index 82df369..f2ad70d 100644 --- a/lib/app/widgets/bantuan_card.dart +++ b/lib/app/widgets/bantuan_card.dart @@ -112,6 +112,19 @@ class BantuanCard extends StatelessWidget { vertical: 4, ), ), + if (item.statusPenyaluran != null && + item.statusPenyaluran!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: _buildPenyaluranStatusBadge( + item.statusPenyaluran!, + fontSize: 10, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + ), + ), const SizedBox(height: 8), Text( formattedJumlah, @@ -159,8 +172,14 @@ class BantuanCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - StatusBadge( - status: item.statusPenerimaan ?? 'MENUNGGU', + Wrap( + spacing: 8, + children: [ + StatusBadge(status: item.statusPenyaluran ?? ""), + StatusBadge( + status: item.statusPenerimaan ?? 'MENUNGGU', + ), + ], ), ], ), @@ -276,4 +295,51 @@ class BantuanCard extends StatelessWidget { ), ); } + + // Widget untuk menampilkan badge status penyaluran + Widget _buildPenyaluranStatusBadge( + String status, { + double fontSize = 12, + EdgeInsets padding = + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + }) { + Color statusColor; + String statusText; + + switch (status.toUpperCase()) { + case 'DIJADWALKAN': + case 'DISETUJUI': + statusColor = Colors.blue; + statusText = 'Dijadwalkan'; + break; + case 'TERLAKSANA': + statusColor = Colors.green; + statusText = 'Terlaksana'; + break; + case 'BATALTERLAKSANA': + statusColor = Colors.red; + statusText = 'Dibatalkan'; + break; + default: + statusColor = Colors.grey; + statusText = status; + } + + return Container( + padding: padding, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: statusColor), + ), + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: fontSize, + ), + ), + ); + } } diff --git a/lib/app/widgets/status_badge.dart b/lib/app/widgets/status_badge.dart index 30af76c..dff4d5f 100644 --- a/lib/app/widgets/status_badge.dart +++ b/lib/app/widgets/status_badge.dart @@ -30,6 +30,10 @@ class StatusBadge extends StatelessWidget { 'TINDAKAN': Colors.orange, 'SELESAI': Colors.green, 'TERVERIFIKASI': Colors.green, + 'BELUMMENERIMA': Colors.orange, + 'DIJADWALKAN': Colors.blue, + 'TERLAKSANA': Colors.purple, + 'AKTIF': Colors.green, }; // Default labels for common statuses @@ -42,6 +46,10 @@ class StatusBadge extends StatelessWidget { 'TINDAKAN': 'Tindakan', 'SELESAI': 'Selesai', 'TERVERIFIKASI': 'Terverifikasi', + 'BELUMMENERIMA': 'Belum Menerima', + 'DIJADWALKAN': 'Dijadwalkan', + 'TERLAKSANA': 'Terlaksana', + 'AKTIF': 'Aktif', }; // Determine color and label based on status