From 3f785141752472f9975b7ed65fa19245eb8d9d0f Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Sun, 30 Mar 2025 20:05:22 +0700 Subject: [PATCH] Perbarui model LokasiPenyaluran dengan mengganti properti alamat menjadi alamatLengkap. Modifikasi tampilan dan controller di modul donatur dan petugas desa untuk menggunakan properti baru ini. Tambahkan fungsionalitas untuk mengelola lokasi penyaluran, termasuk penghapusan dan pengeditan lokasi. Perbarui rute aplikasi untuk menambahkan halaman lokasi penyaluran baru dan pastikan controller terdaftar dengan benar. --- JadwalPenyaluranController()) | 0 .../arm64-v8a/configure_fingerprint.bin | 24 +- .../armeabi-v7a/configure_fingerprint.bin | 24 +- .../626b5o2n/x86/configure_fingerprint.bin | 24 +- .../626b5o2n/x86_64/configure_fingerprint.bin | 24 +- .../data/models/lokasi_penyaluran_model.dart | 8 +- .../donatur/views/donatur_penitipan_view.dart | 2 +- .../detail_penyaluran_controller.dart | 3 +- .../jadwal_penyaluran_controller.dart | 108 +- .../views/detail_penyaluran_page.dart | 1346 ++++++++++++----- .../views/lokasi_penyaluran_view.dart | 507 +++++++ .../petugas_desa/views/penyaluran_view.dart | 247 +-- .../petugas_desa/views/petugas_desa_view.dart | 9 + .../views/tambah_lokasi_penyaluran_view.dart | 59 +- lib/app/routes/app_pages.dart | 6 + lib/app/routes/app_routes.dart | 2 + lib/app/services/supabase_service.dart | 28 + 17 files changed, 1706 insertions(+), 715 deletions(-) create mode 100644 JadwalPenyaluranController()) create mode 100644 lib/app/modules/petugas_desa/views/lokasi_penyaluran_view.dart diff --git a/JadwalPenyaluranController()) b/JadwalPenyaluranController()) new file mode 100644 index 0000000..e69de29 diff --git a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin index 9115535..693bc59 100644 --- a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin +++ b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin @@ -2,27 +2,27 @@ C/C++ Structured LogO M KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC A -?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  2 2 +?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  Ƹ2 2  -}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  2  2~ +}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  Ƹ2  2~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  2 2 +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  Ƹ2 2  -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  2 2p +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  Ƹ2 2p n -lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  2 2t +lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  Ƹ2 2t r -pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  2y +pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  Ƹ2y w -uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  2 K 2z +uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  Ƹ2 K 2z x -vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  2 ~ +vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  Ƹ2 ~ | -zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  2 +zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  Ƹ2   -D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  2  2w +D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  Ƹ2  2w u -sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  2  ( 2| +sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  Ƹ2  ( 2| z -xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\symbol_folder_index.txt  2  o 2 \ No newline at end of file +xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\symbol_folder_index.txt  Ƹ2  o 2 \ No newline at end of file diff --git a/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/armeabi-v7a/configure_fingerprint.bin index 0f04b47..4488132 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 149cb05..113b1ac 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 8bdbdfb..0375bf0 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/lokasi_penyaluran_model.dart b/lib/app/data/models/lokasi_penyaluran_model.dart index 4275bd4..63cbe21 100644 --- a/lib/app/data/models/lokasi_penyaluran_model.dart +++ b/lib/app/data/models/lokasi_penyaluran_model.dart @@ -3,7 +3,7 @@ import 'dart:convert'; class LokasiPenyaluranModel { final String id; final String nama; - final String? alamat; + final String? alamatLengkap; final String? desa; final String? kecamatan; final String? kabupaten; @@ -19,7 +19,7 @@ class LokasiPenyaluranModel { LokasiPenyaluranModel({ required this.id, required this.nama, - this.alamat, + this.alamatLengkap, this.desa, this.kecamatan, this.kabupaten, @@ -42,7 +42,7 @@ class LokasiPenyaluranModel { LokasiPenyaluranModel( id: json["id"], nama: json["nama"], - alamat: json["alamat"], + alamatLengkap: json["alamat_lengkap"], desa: json["desa"], kecamatan: json["kecamatan"], kabupaten: json["kabupaten"], @@ -61,7 +61,7 @@ class LokasiPenyaluranModel { Map toJson() => { "id": id, "nama": nama, - "alamat": alamat, + "alamat_lengkap": alamatLengkap, "desa": desa, "kecamatan": kecamatan, "kabupaten": kabupaten, diff --git a/lib/app/modules/donatur/views/donatur_penitipan_view.dart b/lib/app/modules/donatur/views/donatur_penitipan_view.dart index 0d7d70c..a07031a 100644 --- a/lib/app/modules/donatur/views/donatur_penitipan_view.dart +++ b/lib/app/modules/donatur/views/donatur_penitipan_view.dart @@ -1196,7 +1196,7 @@ class _FormPenitipanBantuanState extends State { value: selectedLokasiPenyaluranId, items: controller.lokasiPenyaluran.map((lokasi) { String alamatLengkap = [ - lokasi.alamat, + lokasi.alamatLengkap, lokasi.desa, lokasi.kecamatan, lokasi.kabupaten, diff --git a/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart index 613ed9e..c895827 100644 --- a/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart @@ -101,7 +101,8 @@ class DetailPenyaluranController extends GetxController { // Ambil data penerima penyaluran final penerimaPenyaluranData = await _supabaseService.client .from('penerima_penyaluran') - .select('*, warga:warga_id(*)') + .select( + '*, warga:warga_id(*, desa:desa_id(*)), stok_bantuan:stok_bantuan_id(*)') .eq('penyaluran_bantuan_id', penyaluranId); final List penerima = []; 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 4cb1f86..40eedf7 100644 --- a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -588,19 +588,21 @@ class JadwalPenyaluranController extends GetxController { } Future loadLokasiPenyaluranData() async { + isLokasiLoading.value = true; try { - isLokasiLoading(true); - final lokasiData = await _supabaseService.getAllLokasiPenyaluran(); - if (lokasiData != null) { - for (var lokasi in lokasiData) { - final lokasiModel = LokasiPenyaluranModel.fromJson(lokasi); - lokasiPenyaluranCache[lokasiModel.id] = lokasiModel; - } + final data = await _supabaseService.getLokasiPenyaluran( + petugasId: user?.id, + ); + + // Bersihkan cache dan tambahkan data baru + lokasiPenyaluranCache.clear(); + for (final lokasi in data) { + lokasiPenyaluranCache[lokasi.id] = lokasi; } } catch (e) { - print('Error loading lokasi penyaluran data: $e'); + print('Error loading lokasi penyaluran: $e'); } finally { - isLokasiLoading(false); + isLokasiLoading.value = false; } } @@ -869,4 +871,92 @@ class JadwalPenyaluranController extends GetxController { // Kembalikan representasi string dari hash return hash.toString(); } + + // Mengedit lokasi penyaluran + void editLokasiPenyaluran(String lokasiId) { + if (lokasiPenyaluranCache.containsKey(lokasiId)) { + // Ambil data lokasi yang akan diedit + final lokasi = lokasiPenyaluranCache[lokasiId]; + + // Navigasi ke halaman edit dengan membawa data lokasi + Get.toNamed('/petugas-desa/edit-lokasi-penyaluran', arguments: { + 'lokasi_id': lokasiId, + 'lokasi': lokasi, + }); + } else { + Get.snackbar( + 'Gagal', + 'Data lokasi tidak ditemukan', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + // Menghapus lokasi penyaluran + void hapusLokasiPenyaluran(String lokasiId) { + // Tampilkan dialog konfirmasi penghapusan + Get.dialog( + AlertDialog( + title: const Text('Konfirmasi Hapus'), + content: const Text('Apakah Anda yakin ingin menghapus lokasi ini?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('BATAL'), + ), + TextButton( + onPressed: () async { + Get.back(); // Tutup dialog + + // Tampilkan loading + Get.dialog( + const Center( + child: CircularProgressIndicator(), + ), + barrierDismissible: false, + ); + + try { + // Lakukan penghapusan di database + await _supabaseService.deleteLokasiPenyaluran(lokasiId); + + // Hapus data dari cache lokal + lokasiPenyaluranCache.remove(lokasiId); + + // Tutup dialog loading + Get.back(); + + // Tampilkan notifikasi berhasil + Get.snackbar( + 'Berhasil', + 'Lokasi penyaluran berhasil dihapus', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } catch (e) { + // Tutup dialog loading + Get.back(); + + // Tampilkan pesan error + Get.snackbar( + 'Gagal', + 'Terjadi kesalahan: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('HAPUS'), + ), + ], + ), + ); + } } 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 74c3bb9..00a5381 100644 --- a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart +++ b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart @@ -307,12 +307,12 @@ class DetailPenyaluranPage extends StatelessWidget { } // Widget untuk info item dengan icon - Widget _buildInfoItem( - IconData icon, String label, String value, Color iconColor) { + Widget _buildInfoItem(IconData icon, String label, String value, + [Color? statusColor]) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 20, color: iconColor), + Icon(icon, size: 20, color: statusColor ?? AppTheme.secondaryColor), const SizedBox(width: 10), Expanded( child: Column( @@ -1330,7 +1330,6 @@ class DetailPenyaluranPage extends StatelessWidget { label: Text( 'Selesaikan', style: TextStyle( - fontWeight: FontWeight.bold, fontSize: allReceived ? 16 : 14, ), ), @@ -1593,6 +1592,7 @@ class DetailPenyaluranPage extends StatelessWidget { showModalBottomSheet( context: context, isScrollControlled: true, + backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), @@ -1600,299 +1600,971 @@ class DetailPenyaluranPage extends StatelessWidget { return Container( padding: const EdgeInsets.all(20), constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.8, + maxHeight: MediaQuery.of(context).size.height * 0.85, ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Handle untuk drag - Center( - child: Container( - width: 50, - height: 5, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(10), - ), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + // Handle untuk drag + Center( + child: Container( + width: 50, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), ), ), - const SizedBox(height: 20), + ), + const SizedBox(height: 16), - // Header dengan avatar dan nama - Row( - children: [ - CircleAvatar( - radius: 30, - backgroundColor: statusColor.withOpacity(0.2), - backgroundImage: warga != null && - warga['foto_profil'] != null && - warga['foto_profil'].toString().isNotEmpty - ? NetworkImage(warga['foto_profil']) - : null, - child: (warga == null || - warga['foto_profil'] == null || - warga['foto_profil'].toString().isEmpty) - ? Text( - warga != null && warga['nama_lengkap'] != null - ? warga['nama_lengkap'] - .toString() - .substring(0, 1) - .toUpperCase() - : '?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: statusColor, - fontSize: 24, - ), - ) - : null, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - warga != null - ? warga['nama_lengkap'] ?? 'Nama tidak tersedia' - : 'Nama tidak tersedia', - style: const TextStyle( - fontSize: 20, + // Konten utama dengan scrolling + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section 1: Header dengan avatar dan informasi utama + _buildDetailHeader(warga, sudahMenerima, statusColor), + + const SizedBox(height: 24), + + // Section 2: Biodata lengkap + _buildDetailBiodata(warga), + + const SizedBox(height: 16), + + // Section 3: Informasi penerimaan bantuan + _buildDetailInfoPenerimaan(penerima, statusColor), + + // Section 4: Bukti penerimaan (jika ada) + if (penerima.buktiPenerimaan != null && + penerima.buktiPenerimaan!.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildDetailBuktiPenerimaan(penerima.buktiPenerimaan!), + ], + ], + ), + ), + ), + + // Tombol aksi + _buildDetailActionButtons(context, penerima, sudahMenerima), + ], + ), + ); + }, + ); + } + + // Widget untuk header detail penerima + Widget _buildDetailHeader( + Map? warga, bool sudahMenerima, Color statusColor) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.08), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + children: [ + // Avatar dan nama + Row( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 36, + backgroundColor: statusColor.withOpacity(0.15), + backgroundImage: warga != null && + warga['foto_profil'] != null && + warga['foto_profil'].toString().isNotEmpty + ? NetworkImage(warga['foto_profil']) + : null, + child: (warga == null || + warga['foto_profil'] == null || + warga['foto_profil'].toString().isEmpty) + ? Text( + warga != null && warga['nama_lengkap'] != null + ? warga['nama_lengkap'] + .toString() + .substring(0, 1) + .toUpperCase() + : '?', + style: TextStyle( fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: statusColor.withOpacity(0.3)), - ), - child: Text( - sudahMenerima - ? 'Sudah Menerima' - : 'Belum Menerima', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: statusColor, - ), + color: statusColor, + fontSize: 28, ), + ) + : null, + ), + Positioned( + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 0, ), ], ), + child: Icon( + sudahMenerima ? Icons.check_circle : Icons.pending, + color: statusColor, + size: 18, + ), + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nama dengan overflow ellipsis + Text( + warga != null && warga['nama_lengkap'] != null + ? warga['nama_lengkap'] + : 'Nama tidak tersedia', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 4), + + // NIK dengan icon + Row( + children: [ + Icon(Icons.credit_card_outlined, + size: 14, color: Colors.grey.shade600), + const SizedBox(width: 4), + Expanded( + child: Text( + warga != null && warga['nik'] != null + ? 'NIK: ${warga['nik']}' + : 'NIK: -', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ], ), + ), + ], + ), - const SizedBox(height: 24), + const SizedBox(height: 16), - // Informasi biodata - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Biodata Singkat', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - const Divider(height: 24), - if (warga != null) ...[ - _buildInfoRow('NIK', warga['nik'] ?? '-'), - _buildInfoRow('Alamat', warga['alamat'] ?? '-'), - _buildInfoRow('Desa', warga['desa'] ?? '-'), - _buildInfoRow('Kecamatan', warga['kecamatan'] ?? '-'), - _buildInfoRow('Kabupaten', warga['kabupaten'] ?? '-'), - _buildInfoRow('Provinsi', warga['provinsi'] ?? '-'), - _buildInfoRow( - 'Jenis Kelamin', warga['jenis_kelamin'] ?? '-'), - _buildInfoRow('No. Telepon', warga['no_hp'] ?? '-'), - ], - ], - ), + // Status + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + sudahMenerima ? Icons.check_circle : Icons.pending_outlined, + color: statusColor, + size: 20, ), - - const SizedBox(height: 16), - - // Informasi penerimaan - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: statusColor.withOpacity(0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - sudahMenerima ? Icons.check_circle : Icons.pending, - color: statusColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Informasi Penerimaan', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: statusColor, - ), - ), - ], - ), - const Divider(height: 24), - _buildInfoRow( - 'Status Penerimaan', - _getStatusPenerimaanText( - penerima.statusPenerimaan ?? '-')), - if (penerima.tanggalPenerimaan != null) - _buildInfoRow( - 'Tanggal Penerimaan', - FormatHelper.formatDateTime( - penerima.tanggalPenerimaan!)), - if (penerima.jumlahBantuan != null) - _buildInfoRow('Jumlah Bantuan', - penerima.jumlahBantuan.toString()), - ], - ), - ), - - // Bukti penerimaan - if (penerima.buktiPenerimaan != null && - penerima.buktiPenerimaan!.isNotEmpty) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Bukti Penerimaan', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - penerima.buktiPenerimaan!, - height: 200, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 200, - width: double.infinity, - color: Colors.grey[300], - child: const Center( - child: Text('Gagal memuat gambar'), - ), - ); - }, - ), - ), - ], - ), - ), - ], - - const SizedBox(height: 30), - - // Tombol konfirmasi penerimaan - if (controller.penyaluran.value?.status?.toUpperCase() == - 'AKTIF' && - penerima.statusPenerimaan?.toUpperCase() != 'DITERIMA') ...[ - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.check_circle), - label: const Text( - 'Konfirmasi Penerimaan', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.successColor, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - Navigator.pop(context); - _showKonfirmasiPenerimaan(context, penerima); - }, - ), - ), - const SizedBox(height: 10), - ], - - // Tombol tutup - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey.shade200, - foregroundColor: Colors.black87, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - 'Tutup', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), + const SizedBox(width: 8), + Text( + sudahMenerima + ? 'Sudah Menerima Bantuan' + : 'Belum Menerima Bantuan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: statusColor, ), ), ], ), ), - ); - }, + ], + ), ); } + // Widget untuk biodata + Widget _buildDetailBiodata(Map? warga) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header section + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.person_outline, + color: AppTheme.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + 'Biodata Penerima', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + + const Divider(height: 24), + + if (warga != null) ...[ + // Data alamat lengkap + _buildInfoGroup( + 'Alamat Lengkap', + [ + _buildInfoItemGroup('Alamat', warga['alamat'] ?? '-'), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildInfoItemGroup( + 'Desa', + warga['desa'] != null && warga['desa'] is Map + ? warga['desa']['nama'] ?? '-' + : warga['nama_desa'] ?? warga['desa'] ?? '-'), + ), + Expanded( + child: _buildInfoItemGroup( + 'Kecamatan', + warga['desa'] != null && warga['desa'] is Map + ? warga['desa']['kecamatan'] ?? '-' + : warga['kecamatan'] ?? '-'), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildInfoItemGroup( + 'Kabupaten', + warga['desa'] != null && warga['desa'] is Map + ? warga['desa']['kabupaten'] ?? '-' + : warga['kabupaten'] ?? '-'), + ), + Expanded( + child: _buildInfoItemGroup( + 'Provinsi', + warga['desa'] != null && warga['desa'] is Map + ? warga['desa']['provinsi'] ?? '-' + : warga['provinsi'] ?? '-'), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Data personal + _buildInfoGroup( + 'Data Personal', + [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildInfoItemGroup( + 'Jenis Kelamin', warga['jenis_kelamin'] ?? '-'), + ), + Expanded( + child: _buildInfoItemGroup( + 'No. Telepon', warga['no_hp'] ?? '-'), + ), + ], + ), + ], + ), + ], + ], + ), + ), + ); + } + + // Widget untuk informasi penerimaan + Widget _buildDetailInfoPenerimaan( + PenerimaPenyaluranModel penerima, Color statusColor) { + final bool sudahMenerima = + penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA'; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: statusColor.withOpacity(0.3)), + ), + color: statusColor.withOpacity(0.03), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + sudahMenerima + ? Icons.verified_outlined + : Icons.pending_actions_outlined, + color: statusColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Detail Penerimaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ], + ), + + const Divider(height: 24), + + // Status penerimaan dengan chip + Row( + children: [ + Text( + 'Status:', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const SizedBox(width: 12), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Text( + sudahMenerima ? 'Sudah Menerima' : 'Belum Menerima', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Tanggal penerimaan + if (penerima.tanggalPenerimaan != null) ...[ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Icon( + Icons.calendar_today, + size: 14, + color: statusColor, + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tanggal Penerimaan', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + FormatHelper.formatDateTime( + penerima.tanggalPenerimaan!), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + ], + + // Jenis bantuan + if (penerima.stokBantuan != null && + penerima.stokBantuan!['nama'] != null) ...[ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Icon( + Icons.category_outlined, + size: 14, + color: statusColor, + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jenis Bantuan', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Row( + children: [ + Text( + penerima.stokBantuan!['nama'], + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(width: 6), + if (_getBantuanIsUang(penerima)) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Uang', + style: TextStyle( + fontSize: 11, + color: Colors.green.shade800, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ], + ), + const SizedBox(height: 12), + ], + + // Jumlah bantuan + if (penerima.jumlahBantuan != null) ...[ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Icon( + _getBantuanIsUang(penerima) + ? Icons.monetization_on_outlined + : Icons.inventory_2_outlined, + size: 14, + color: statusColor, + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jumlah Bantuan', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + _formatJumlahBantuan(penerima), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + ], + ], + ), + ), + ); + } + + // Helper untuk mengecek apakah bantuan berupa uang + bool _getBantuanIsUang(PenerimaPenyaluranModel penerima) { + // Cek dari relasi stok_bantuan + if (penerima.stokBantuan != null) { + return penerima.stokBantuan!['is_uang'] ?? false; + } + // Fallback jika stok_bantuan tidak tersedia + return penerima.isUang ?? false; + } + + // Helper untuk memformat jumlah bantuan dengan satuan dan cek apakah uang + String _formatJumlahBantuan(PenerimaPenyaluranModel penerima) { + // Cek apakah berupa uang dan ambil satuan dari stok_bantuan + bool isUang = false; + String satuan = ''; + + // Ambil data dari relasi stok_bantuan + if (penerima.stokBantuan != null) { + isUang = penerima.stokBantuan!['is_uang'] ?? false; + satuan = penerima.stokBantuan!['satuan'] ?? ''; + } else { + // Fallback jika stok_bantuan tidak tersedia + isUang = penerima.isUang ?? false; + satuan = penerima.satuan ?? ''; + } + + // Format jumlah bantuan + if (isUang) { + return FormatHelper.formatRupiah(penerima.jumlahBantuan ?? 0); + } else { + return '${penerima.jumlahBantuan} ${satuan.isNotEmpty ? satuan : 'item'}'; + } + } + + // Widget untuk bukti penerimaan + Widget _buildDetailBuktiPenerimaan(String buktiUrl) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.photo_camera_outlined, + color: AppTheme.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + 'Bukti Penerimaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Foto bukti + Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + buktiUrl, + fit: BoxFit.cover, + 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, + ), + ), + ], + ), + ); + }, + ), + Positioned( + right: 8, + bottom: 8, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.zoom_in, + color: Colors.white, + size: 20, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + // Widget untuk tombol aksi + Widget _buildDetailActionButtons(BuildContext context, + PenerimaPenyaluranModel penerima, bool sudahMenerima) { + return Column( + children: [ + const SizedBox(height: 16), + + // Tombol konfirmasi penerimaan (jika status adalah AKTIF dan belum menerima) + if (controller.penyaluran.value?.status?.toUpperCase() == 'AKTIF' && + !sudahMenerima) ...[ + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.check_circle), + label: const Text( + 'Konfirmasi Penerimaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + onPressed: () { + Navigator.pop(context); + _showKonfirmasiPenerimaan(context, penerima); + }, + ), + ), + const SizedBox(height: 12), + ], + + // Tombol tutup + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon(Icons.close), + label: const Text( + 'Tutup', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + side: BorderSide(color: Colors.grey.shade400), + foregroundColor: Colors.grey.shade700, + ), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ); + } + + // Widget untuk grup informasi + Widget _buildInfoGroup(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ], + ); + } + + // Widget untuk item informasi dalam grup + Widget _buildInfoItemGroup(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ); + } + + List _getFilteredPenerima() { + final query = searchQuery.value; + final status = statusFilter.value; + + // Filter dasar berdasarkan query pencarian + List filteredList = controller.penerimaPenyaluran; + + if (query.isNotEmpty) { + filteredList = filteredList.where((item) { + final warga = item.warga; + if (warga == null) return false; + + final nama = warga['nama_lengkap']?.toString().toLowerCase() ?? ''; + final nik = warga['nik']?.toString().toLowerCase() ?? ''; + final alamat = warga['alamat']?.toString().toLowerCase() ?? ''; + final statusPenerimaan = item.statusPenerimaan?.toLowerCase() ?? ''; + + return nama.contains(query) || + nik.contains(query) || + alamat.contains(query) || + statusPenerimaan.contains(query); + }).toList(); + } + + // Filter tambahan berdasarkan status + if (status != 'SEMUA') { + filteredList = filteredList.where((item) { + if (status == 'DITERIMA') { + return item.statusPenerimaan?.toUpperCase() == 'DITERIMA'; + } else { + // Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA + return item.statusPenerimaan?.toUpperCase() == 'BELUMMENERIMA'; + } + }).toList(); + } + + return filteredList; + } + + // Fungsi untuk membuka scanner QR code + void _showQrCodeScanner(BuildContext context) async { + if (controller.penyaluran.value?.id == null) return; + + final result = await Get.to( + () => QrScannerPage( + penyaluranId: controller.penyaluran.value!.id!, + ), + ); + + if (result == true) { + // Refresh data setelah kembali dari scanner jika berhasil + await controller.refreshData(); + Get.snackbar( + 'Berhasil', + 'Penerima berhasil diverifikasi', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } + + // Widget untuk menampilkan QR Code (dikosongkan untuk petugas desa) + Widget _buildQrCodeSection(PenerimaPenyaluranModel penerima) { + // Widget QR Code tetap dibuat tapi tidak digunakan di petugas desa + return const SizedBox.shrink(); + } + + // Widget untuk status chip baru + Widget _buildStatusChipNew(String status) { + final bool isDiterima = status.toUpperCase() == 'DITERIMA'; + final Color statusColor = + isDiterima ? AppTheme.successColor : AppTheme.warningColor; + final String statusText = isDiterima ? 'Sudah Menerima' : 'Belum Menerima'; + final IconData statusIcon = isDiterima ? Icons.check_circle : Icons.pending; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: statusColor.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: statusColor.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + statusIcon, + size: 14, + color: statusColor, + ), + const SizedBox(width: 5), + Text( + statusText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ], + ), + ); + } + + void _showDetailPenerima( + BuildContext context, PenerimaPenyaluranModel penerima) { + _showDetailPenerimaan(context, penerima); + } + String _getStatusText(String status) { switch (status.toUpperCase()) { case 'DIJADWALKAN': @@ -1908,16 +2580,6 @@ class DetailPenyaluranPage extends StatelessWidget { } } - String _getStatusPenerimaanText(String status) { - // Konversi status ke format yang diinginkan - if (status.toUpperCase() == 'DITERIMA') { - return 'Sudah Menerima'; - } else { - // Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA - return 'Belum Menerima'; - } - } - Widget _buildPembatalanSection(BuildContext context) { final penyaluran = controller.penyaluran.value!; @@ -2202,120 +2864,4 @@ class DetailPenyaluranPage extends StatelessWidget { ); }); } - - List _getFilteredPenerima() { - final query = searchQuery.value; - final status = statusFilter.value; - - // Filter dasar berdasarkan query pencarian - List filteredList = controller.penerimaPenyaluran; - - if (query.isNotEmpty) { - filteredList = filteredList.where((item) { - final warga = item.warga; - if (warga == null) return false; - - final nama = warga['nama_lengkap']?.toString().toLowerCase() ?? ''; - final nik = warga['nik']?.toString().toLowerCase() ?? ''; - final alamat = warga['alamat']?.toString().toLowerCase() ?? ''; - final statusPenerimaan = item.statusPenerimaan?.toLowerCase() ?? ''; - - return nama.contains(query) || - nik.contains(query) || - alamat.contains(query) || - statusPenerimaan.contains(query); - }).toList(); - } - - // Filter tambahan berdasarkan status - if (status != 'SEMUA') { - filteredList = filteredList.where((item) { - if (status == 'DITERIMA') { - return item.statusPenerimaan?.toUpperCase() == 'DITERIMA'; - } else { - // Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA - return item.statusPenerimaan?.toUpperCase() == 'BELUMMENERIMA'; - } - }).toList(); - } - - return filteredList; - } - - // Fungsi untuk membuka scanner QR code - void _showQrCodeScanner(BuildContext context) async { - if (controller.penyaluran.value?.id == null) return; - - final result = await Get.to( - () => QrScannerPage( - penyaluranId: controller.penyaluran.value!.id!, - ), - ); - - if (result == true) { - // Refresh data setelah kembali dari scanner jika berhasil - await controller.refreshData(); - Get.snackbar( - 'Berhasil', - 'Penerima berhasil diverifikasi', - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } - - // Widget untuk menampilkan QR Code (dikosongkan untuk petugas desa) - Widget _buildQrCodeSection(PenerimaPenyaluranModel penerima) { - // Widget QR Code tetap dibuat tapi tidak digunakan di petugas desa - return const SizedBox.shrink(); - } - - // Widget untuk status chip baru - Widget _buildStatusChipNew(String status) { - final bool isDiterima = status.toUpperCase() == 'DITERIMA'; - final Color statusColor = - isDiterima ? AppTheme.successColor : AppTheme.warningColor; - final String statusText = isDiterima ? 'Sudah Menerima' : 'Belum Menerima'; - final IconData statusIcon = isDiterima ? Icons.check_circle : Icons.pending; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: statusColor.withOpacity(0.3)), - boxShadow: [ - BoxShadow( - color: statusColor.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - statusIcon, - size: 14, - color: statusColor, - ), - const SizedBox(width: 5), - Text( - statusText, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: statusColor, - ), - ), - ], - ), - ); - } - - void _showDetailPenerima( - BuildContext context, PenerimaPenyaluranModel penerima) { - _showDetailPenerimaan(context, penerima); - } } diff --git a/lib/app/modules/petugas_desa/views/lokasi_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/lokasi_penyaluran_view.dart new file mode 100644 index 0000000..8b3cd79 --- /dev/null +++ b/lib/app/modules/petugas_desa/views/lokasi_penyaluran_view.dart @@ -0,0 +1,507 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; +import 'package:penyaluran_app/app/routes/app_pages.dart'; + +class LokasiPenyaluranView extends GetView { + const LokasiPenyaluranView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Lokasi Penyaluran'), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + ), + body: _buildLokasiPenyaluranList(), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran), + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.add_location, color: Colors.white), + label: + const Text('Tambah Lokasi', style: TextStyle(color: Colors.white)), + ), + ); + } + + Widget _buildLokasiPenyaluranList() { + return RefreshIndicator( + onRefresh: () => controller.loadLokasiPenyaluranData(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan gradient yang lebih menarik + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.blue.shade700, + Colors.blue.shade500, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade200.withOpacity(0.5), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.location_on, + color: Colors.white.withOpacity(0.9), + size: 24, + ), + const SizedBox(width: 10), + Text( + 'Daftar Lokasi Penyaluran', + style: Theme.of(Get.context!) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 10), + Text( + 'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah', + style: TextStyle( + fontSize: 14, + color: Colors.white.withOpacity(0.9), + ), + ), + const SizedBox(height: 16), + // Counter jumlah lokasi + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + ), + child: Obx(() => Text( + '${controller.lokasiPenyaluranCache.length} Lokasi Terdaftar', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + )), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Daftar Lokasi + Expanded( + child: Obx(() { + if (controller.isLokasiLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (controller.lokasiPenyaluranCache.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.location_off, + size: 64, + color: Colors.grey.shade400, + ), + ), + const SizedBox(height: 16), + Text( + 'Belum ada lokasi penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Tambahkan lokasi penyaluran untuk memulai', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: () => + Get.toNamed(Routes.tambahLokasiPenyaluran), + icon: const Icon(Icons.add_location), + label: const Text('Tambah Lokasi'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + ), + ], + ), + ); + } + + return ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(top: 8, bottom: 100), + itemCount: controller.lokasiPenyaluranCache.length + + 1, // +1 untuk footer + itemBuilder: (context, index) { + // Footer item + if (index == controller.lokasiPenyaluranCache.length) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'Tidak ada lokasi lainnya', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), + ), + ), + ); + } + + // Item lokasi normal + final lokasi = controller.lokasiPenyaluranCache.values + .elementAt(index); + final lokasiId = + controller.lokasiPenyaluranCache.keys.elementAt(index); + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Card( + margin: EdgeInsets.zero, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + // Tambahkan aksi ketika card diklik + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon Lokasi dengan latar belakang + Hero( + tag: 'lokasi_icon_$lokasiId', + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: + Colors.blue.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.location_on, + color: Colors.blue.shade700, + size: 28, + ), + ), + ), + const SizedBox(width: 16), + // Informasi utama lokasi + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Hero( + tag: 'lokasi_nama_$lokasiId', + child: Material( + color: Colors.transparent, + child: Text( + lokasi.nama, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + ), + const SizedBox(height: 4), + if (lokasi.alamatLengkap != null && + lokasi.alamatLengkap!.isNotEmpty) + Text( + lokasi.alamatLengkap!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + // Indikator koordinat tersedia + if (lokasi.latitude != null && + lokasi.longitude != null) + Padding( + padding: const EdgeInsets.only( + top: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.gps_fixed, + size: 14, + color: Colors.blue.shade500, + ), + const SizedBox(width: 4), + Text( + 'Koordinat tersedia', + style: TextStyle( + fontSize: 12, + color: + Colors.blue.shade500, + fontWeight: + FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + + // Badge "NEW" jika lokasi baru dibuat (kurang dari 3 hari) + if (_isNewLocation(lokasi.createdAt)) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.orange.shade500, + borderRadius: + BorderRadius.circular(12), + ), + child: const Text( + 'BARU', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ), + ], + ), + + // Divider dan tag kategori + if (lokasi.isLokasiTitip || + (lokasi.desa != null && + lokasi.desa!.isNotEmpty)) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Divider(color: Colors.grey.shade200), + ), + + // Informasi tambahan dan tag + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + // Tag lokasi penitipan jika ada + if (lokasi.isLokasiTitip) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: + BorderRadius.circular(30), + border: Border.all( + color: Colors.green.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle_outline, + size: 16, + color: Colors.green.shade800, + ), + const SizedBox(width: 4), + Text( + 'Lokasi Penitipan', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.green.shade800, + ), + ), + ], + ), + ), + + // Tag informasi desa jika ada + if (lokasi.desa != null && + lokasi.desa!.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: + BorderRadius.circular(30), + border: Border.all( + color: Colors.blue.shade200), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.location_city, + size: 16, + color: Colors.blue.shade800, + ), + const SizedBox(width: 4), + Text( + lokasi.desa!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + ), + ], + ), + ), + + // Tombol aksi + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Tombol Hapus + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + // Aksi hapus lokasi + controller.hapusLokasiPenyaluran( + lokasiId); + }, + borderRadius: + BorderRadius.circular(30), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.delete_outline, + color: Colors.red.shade600, + size: 18), + const SizedBox(width: 4), + Text( + 'Hapus', + style: TextStyle( + color: Colors.red.shade600, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }), + ), + ], + ), + ), + ); + } + + // Helper method untuk menentukan apakah lokasi baru dibuat (kurang dari 3 hari) + bool _isNewLocation(DateTime createdAt) { + final now = DateTime.now(); + final difference = now.difference(createdAt); + return difference.inDays < 3; + } +} diff --git a/lib/app/modules/petugas_desa/views/penyaluran_view.dart b/lib/app/modules/petugas_desa/views/penyaluran_view.dart index 5ad1507..71dd71e 100644 --- a/lib/app/modules/petugas_desa/views/penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/penyaluran_view.dart @@ -5,12 +5,16 @@ import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart'; -import 'package:penyaluran_app/app/routes/app_pages.dart'; class PenyaluranView extends GetView { const PenyaluranView({super.key}); @override Widget build(BuildContext context) { + // Memastikan controller tersedia + if (!Get.isRegistered()) { + Get.put(JadwalPenyaluranController()); + } + return DefaultTabController( length: 2, child: Scaffold( @@ -84,11 +88,6 @@ class PenyaluranView extends GetView { // Ringkasan jadwal _buildJadwalSummary(Get.context!), - const SizedBox(height: 16), - - // Tombol untuk mengelola lokasi penyaluran - _buildLokasiPenyaluranSection(), - const SizedBox(height: 24), // Jadwal hari ini @@ -237,240 +236,4 @@ class PenyaluranView extends GetView { ], ); } - - // Widget untuk menampilkan section lokasi penyaluran - Widget _buildLokasiPenyaluranSection() { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: Colors.blue.shade100, width: 1), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Lokasi Penyaluran', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - OutlinedButton.icon( - onPressed: () { - // Menampilkan dialog daftar lokasi penyaluran - _showLokasiPenyaluranDialog(); - }, - icon: const Icon(Icons.map, size: 16), - label: const Text('Lihat Lokasi'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.blue, - side: BorderSide(color: Colors.blue.shade300), - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran), - icon: const Icon(Icons.add_location, size: 16), - label: const Text('Tambah Lokasi Penyaluran Baru'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade50, - foregroundColor: Colors.blue.shade700, - padding: - const EdgeInsets.symmetric(vertical: 10, horizontal: 12), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: Colors.blue.shade200), - ), - ), - ), - ], - ), - ), - ); - } - - // Fungsi untuk menampilkan dialog daftar lokasi penyaluran - void _showLokasiPenyaluranDialog() { - Get.dialog( - Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Daftar Lokasi Penyaluran', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - IconButton( - onPressed: () => Get.back(), - icon: const Icon(Icons.close), - visualDensity: VisualDensity.compact, - ), - ], - ), - const SizedBox(height: 12), - Container( - constraints: BoxConstraints( - maxHeight: Get.height * 0.5, - ), - width: double.infinity, - child: Obx(() { - if (controller.isLokasiLoading.value) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (controller.lokasiPenyaluranCache.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.location_off, - size: 48, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Belum ada lokasi penyaluran', - style: TextStyle( - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 8), - ElevatedButton.icon( - onPressed: () { - Get.back(); - Get.toNamed(Routes.tambahLokasiPenyaluran); - }, - icon: const Icon(Icons.add_location), - label: const Text('Tambah Lokasi'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ), - ], - ), - ); - } - - return ListView.builder( - shrinkWrap: true, - itemCount: controller.lokasiPenyaluranCache.length, - itemBuilder: (context, index) { - final lokasi = controller.lokasiPenyaluranCache.values - .elementAt(index); - final lokasiId = controller.lokasiPenyaluranCache.keys - .elementAt(index); - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - title: Text( - lokasi.nama, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (lokasi.alamat != null && - lokasi.alamat!.isNotEmpty) - Text(lokasi.alamat!), - Row( - children: [ - if (lokasi.isLokasiTitip) - Container( - margin: const EdgeInsets.only(top: 4), - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.green.shade100, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'Lokasi Penitipan', - style: TextStyle( - fontSize: 10, - color: Colors.green.shade800, - ), - ), - ), - ], - ), - ], - ), - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - shape: BoxShape.circle, - ), - child: Icon( - Icons.location_on, - color: Colors.blue.shade700, - ), - ), - ), - ); - }, - ); - }), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () { - Get.back(); - Get.toNamed(Routes.tambahLokasiPenyaluran); - }, - child: const Text('Tambah Lokasi Baru'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.blue, - ), - ), - ], - ), - ], - ), - ), - ), - ); - } } diff --git a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart index 075af80..f11c00d 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -386,6 +386,15 @@ class PetugasDesaView extends GetView { Get.toNamed('/daftar-donatur'); }, ), + _buildMenuItem( + icon: Icons.location_on_outlined, + activeIcon: Icons.location_on, + title: 'Lokasi Penyaluran', + onTap: () { + Navigator.pop(context); + Get.toNamed('/lokasi-penyaluran'); + }, + ), _buildMenuItem( icon: Icons.description_outlined, activeIcon: Icons.description, diff --git a/lib/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart index 79facf7..550f061 100644 --- a/lib/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart @@ -15,16 +15,28 @@ class TambahLokasiPenyaluranView extends GetView { backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, ), - body: _buildTambahLokasiPenyaluranForm(context), + body: _LokasiPenyaluranForm(controller: controller), ); } +} - Widget _buildTambahLokasiPenyaluranForm(BuildContext context) { - final formKey = GlobalKey(); - final TextEditingController namaController = TextEditingController(); - final TextEditingController alamatLengkapController = - TextEditingController(); +class _LokasiPenyaluranForm extends StatefulWidget { + final JadwalPenyaluranController controller; + const _LokasiPenyaluranForm({required this.controller}); + + @override + State<_LokasiPenyaluranForm> createState() => _LokasiPenyaluranFormState(); +} + +class _LokasiPenyaluranFormState extends State<_LokasiPenyaluranForm> { + final formKey = GlobalKey(); + final TextEditingController namaController = TextEditingController(); + final TextEditingController alamatLengkapController = TextEditingController(); + bool isLokasiTitip = false; + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: Form( @@ -95,6 +107,29 @@ class TambahLokasiPenyaluranView extends GetView { return null; }, ), + const SizedBox(height: 16), + + // Checkbox Is Lokasi Titip + Row( + children: [ + Checkbox( + value: isLokasiTitip, + activeColor: AppTheme.primaryColor, + onChanged: (newValue) { + setState(() { + isLokasiTitip = newValue ?? false; + }); + }, + ), + const Text('Lokasi Titip'), + const SizedBox(width: 4), + const Tooltip( + message: + 'Centang jika lokasi ini merupakan lokasi yang dapat menerima penitipan', + child: Icon(Icons.info_outline, size: 16), + ), + ], + ), const SizedBox(height: 24), // Tombol Submit @@ -107,6 +142,7 @@ class TambahLokasiPenyaluranView extends GetView { _tambahLokasiPenyaluran( nama: namaController.text, alamatLengkap: alamatLengkapController.text, + isLokasiTitip: isLokasiTitip, ); } }, @@ -137,6 +173,7 @@ class TambahLokasiPenyaluranView extends GetView { Future _tambahLokasiPenyaluran({ required String nama, required String alamatLengkap, + required bool isLokasiTitip, }) async { try { // Tampilkan loading @@ -152,7 +189,8 @@ class TambahLokasiPenyaluranView extends GetView { final String id = uuid.v4(); // Ambil ID petugas desa yang sedang login dari controller - final String? petugasDesaId = controller.supabaseService.currentUser?.id; + final String? petugasDesaId = + widget.controller.supabaseService.currentUser?.id; if (petugasDesaId == null) { Get.back(); // Tutup dialog loading @@ -167,7 +205,7 @@ class TambahLokasiPenyaluranView extends GetView { // Dapatkan desa_id dari data petugas desa // Ambil data petugas desa dari Supabase untuk mendapatkan desa_id - final petugasDesaData = await controller.supabaseService.client + final petugasDesaData = await widget.controller.supabaseService.client .from('petugas_desa') .select('desa_id') .eq('id', petugasDesaId) @@ -193,11 +231,12 @@ class TambahLokasiPenyaluranView extends GetView { 'nama': nama, 'alamat_lengkap': alamatLengkap, 'desa_id': desaId, + 'is_lokasi_titip': isLokasiTitip, 'created_at': DateTime.now().toIso8601String(), }; // Insert data ke tabel lokasi_penyaluran - await controller.supabaseService.client + await widget.controller.supabaseService.client .from('lokasi_penyaluran') .insert(data); @@ -216,7 +255,7 @@ class TambahLokasiPenyaluranView extends GetView { Get.back(); // Refresh data di controller - controller.refreshData(); + widget.controller.refreshData(); } catch (e) { // Tutup dialog loading Get.back(); diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index f78e9ce..c473313 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -13,6 +13,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_vie import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/views/lokasi_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart'; @@ -155,6 +156,11 @@ class AppPages { page: () => const TambahLokasiPenyaluranView(), binding: PetugasDesaBinding(), ), + GetPage( + name: _Paths.lokasiPenyaluran, + page: () => const LokasiPenyaluranView(), + binding: PetugasDesaBinding(), + ), GetPage( name: _Paths.detailPenyaluran, page: () => DetailPenyaluranPage(), diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index a59a202..32e76b7 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -30,6 +30,7 @@ abstract class Routes { static const detailDonatur = _Paths.detailDonatur; static const tambahPenyaluran = _Paths.tambahPenyaluran; static const tambahLokasiPenyaluran = _Paths.tambahLokasiPenyaluran; + static const lokasiPenyaluran = _Paths.lokasiPenyaluran; static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran; static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran; static const laporanPenyaluran = _Paths.laporanPenyaluran; @@ -78,6 +79,7 @@ abstract class _Paths { static const detailDonatur = '/daftar-donatur/detail'; static const tambahPenyaluran = '/tambah-penyaluran'; static const tambahLokasiPenyaluran = '/tambah-lokasi-penyaluran'; + static const lokasiPenyaluran = '/lokasi-penyaluran'; static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran'; static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran'; static const laporanPenyaluran = '/laporan-penyaluran'; diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index d2a8a97..f7f0bc8 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -3,6 +3,7 @@ import 'package:penyaluran_app/app/data/models/donatur_model.dart'; import 'package:penyaluran_app/app/data/models/petugas_desa_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/warga_model.dart'; +import 'package:penyaluran_app/app/data/models/lokasi_penyaluran_model.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:io'; @@ -1736,6 +1737,33 @@ class SupabaseService extends GetxService { } } + // Metode untuk mendapatkan lokasi penyaluran berdasarkan ID petugas + Future> getLokasiPenyaluran( + {String? petugasId}) async { + try { + var query = client.from('lokasi_penyaluran').select('*'); + + final response = await query.order('nama'); + + return response + .map((data) => LokasiPenyaluranModel.fromJson(data)) + .toList(); + } catch (e) { + print('Error getting lokasi penyaluran: $e'); + return []; + } + } + + // Metode untuk menghapus lokasi penyaluran + Future deleteLokasiPenyaluran(String lokasiId) async { + try { + await client.from('lokasi_penyaluran').delete().eq('id', lokasiId); + } catch (e) { + print('Error deleting lokasi penyaluran: $e'); + throw e.toString(); + } + } + // Metode untuk mendapatkan daftar penerima penyaluran berdasarkan ID penyaluran Future>?> getPenerimaPenyaluran( String penyaluranId) async {