From c008020705e224eddd2fd71c4e2a7b1df624b125 Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Thu, 27 Mar 2025 22:31:14 +0700 Subject: [PATCH] membuat tampilan lebih menarik --- .../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/penyaluran_bantuan_model.dart | 40 + lib/app/data/models/skema_bantuan_model.dart | 6 + .../donatur_dashboard_controller.dart | 27 +- .../views/donatur_jadwal_detail_view.dart | 578 +++++ .../donatur/views/donatur_jadwal_view.dart | 394 ++- .../donatur/views/donatur_penitipan_view.dart | 103 + .../donatur/views/donatur_skema_view.dart | 534 +++- .../laporan_penyaluran_controller.dart | 2 +- .../views/laporan_penyaluran_create_view.dart | 5 +- .../views/laporan_penyaluran_detail_view.dart | 2 +- .../views/laporan_penyaluran_view.dart | 571 ++-- .../components/calendar_view_widget.dart | 396 ++- .../components/jadwal_section_widget.dart | 240 +- .../jadwal_penyaluran_controller.dart | 2 +- .../pelaksanaan_penyaluran_controller.dart | 2 +- .../controllers/penerima_controller.dart | 2 +- .../controllers/pengaduan_controller.dart | 26 + .../petugas_desa/views/dashboard_view.dart | 2 +- .../views/detail_donatur_view.dart | 2 +- .../views/detail_penerima_view.dart | 2 +- .../views/detail_pengaduan_view.dart | 2310 ++++++++++------- .../views/detail_penyaluran_page.dart | 1191 ++++++--- .../views/konfirmasi_penerima_page.dart | 2 +- .../petugas_desa/views/pengaduan_view.dart | 2 +- .../petugas_desa/views/penitipan_view.dart | 2 +- .../petugas_desa/views/penyaluran_view.dart | 34 +- .../views/riwayat_pengaduan_view.dart | 547 ++-- .../views/riwayat_penitipan_view.dart | 475 ++-- .../views/riwayat_penyaluran_view.dart | 2 +- .../petugas_desa/views/riwayat_stok_view.dart | 56 +- .../petugas_desa/views/stok_bantuan_view.dart | 8 +- .../views/tambah_penyaluran_view.dart | 2 +- .../warga/views/detail_pengaduan_view.dart | 1406 +++++++--- .../warga/views/warga_dashboard_view.dart | 287 -- .../warga/views/warga_pengaduan_view.dart | 2 +- lib/app/modules/warga/views/warga_view.dart | 16 - lib/app/routes/app_pages.dart | 6 + lib/app/utils/date_helper.dart | 43 + ...te_time_helper.dart => format_helper.dart} | 30 + .../dialogs/detail_penitipan_dialog.dart | 2 +- 44 files changed, 6260 insertions(+), 3195 deletions(-) create mode 100644 lib/app/modules/donatur/views/donatur_jadwal_detail_view.dart create mode 100644 lib/app/utils/date_helper.dart rename lib/app/utils/{date_time_helper.dart => format_helper.dart} (87%) 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 6cb9aff..705724b 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 0e41553..ef82628 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 c374897..8c72b90 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 73ed389..74afe9a 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/penyaluran_bantuan_model.dart b/lib/app/data/models/penyaluran_bantuan_model.dart index 8bdd4b2..ce487a7 100644 --- a/lib/app/data/models/penyaluran_bantuan_model.dart +++ b/lib/app/data/models/penyaluran_bantuan_model.dart @@ -16,6 +16,10 @@ class PenyaluranBantuanModel { final DateTime? tanggalSelesai; final DateTime? createdAt; final DateTime? updatedAt; + final Map? lokasiPenyaluran; + final Map? kategori; + final Map? petugas; + final int? jumlahBantuan; PenyaluranBantuanModel({ this.id, @@ -33,8 +37,40 @@ class PenyaluranBantuanModel { this.tanggalSelesai, this.createdAt, this.updatedAt, + this.lokasiPenyaluran, + this.kategori, + this.petugas, + this.jumlahBantuan, }); + // Mendapatkan nama lokasi dari relasi lokasiPenyaluran + String? get lokasiNama { + if (lokasiPenyaluran != null && lokasiPenyaluran!['nama'] != null) { + return lokasiPenyaluran!['nama']; + } + return null; + } + + // Mendapatkan nama kategori dari relasi kategori + String? get kategoriNama { + if (kategori != null && kategori!['nama'] != null) { + return kategori!['nama']; + } + return null; + } + + // Mendapatkan nama petugas dari relasi petugas + String? get namaPetugas { + if (petugas != null) { + if (petugas!['nama_lengkap'] != null) { + return petugas!['nama_lengkap']; + } else if (petugas!['nama'] != null) { + return petugas!['nama']; + } + } + return null; + } + factory PenyaluranBantuanModel.fromRawJson(String str) => PenyaluranBantuanModel.fromJson(json.decode(str)); @@ -67,6 +103,10 @@ class PenyaluranBantuanModel { updatedAt: json["updated_at"] != null ? DateTime.parse(json["updated_at"]).toUtc() : null, + lokasiPenyaluran: json["lokasi_penyaluran"], + kategori: json["kategori"], + petugas: json["petugas"], + jumlahBantuan: json["jumlah_bantuan"], ); Map toJson() => { diff --git a/lib/app/data/models/skema_bantuan_model.dart b/lib/app/data/models/skema_bantuan_model.dart index 750185d..c86da42 100644 --- a/lib/app/data/models/skema_bantuan_model.dart +++ b/lib/app/data/models/skema_bantuan_model.dart @@ -12,6 +12,7 @@ class SkemaBantuanModel { final String? stokBantuanId; final String? kategoriBantuanId; final double? jumlahDiterimaPerOrang; + final DateTime? batasWaktu; SkemaBantuanModel({ this.id, @@ -25,6 +26,7 @@ class SkemaBantuanModel { this.stokBantuanId, this.kategoriBantuanId, this.jumlahDiterimaPerOrang, + this.batasWaktu, }); factory SkemaBantuanModel.fromRawJson(String str) => @@ -49,6 +51,9 @@ class SkemaBantuanModel { stokBantuanId: json["stok_bantuan_id"], kategoriBantuanId: json["kategori_bantuan_id"], jumlahDiterimaPerOrang: json["jumlah_diterima_per_orang"]?.toDouble(), + batasWaktu: json["batas_waktu"] != null + ? DateTime.parse(json["batas_waktu"]) + : null, ); Map toJson() => { @@ -63,5 +68,6 @@ class SkemaBantuanModel { "stok_bantuan_id": stokBantuanId, "kategori_bantuan_id": kategoriBantuanId, "jumlah_diterima_per_orang": jumlahDiterimaPerOrang, + "batas_waktu": batasWaktu?.toIso8601String(), }; } diff --git a/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart b/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart index 5d100b5..172b716 100644 --- a/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart +++ b/lib/app/modules/donatur/controllers/donatur_dashboard_controller.dart @@ -24,6 +24,9 @@ class DonaturDashboardController extends GetxController { // Indeks tab yang aktif di bottom navigation bar final RxInt activeTabIndex = 0.obs; + // Menyimpan ID skema bantuan yang dipilih + final RxString selectedSkemaBantuanId = ''.obs; + // Data untuk skema bantuan tersedia final RxList skemaBantuan = [].obs; @@ -228,7 +231,8 @@ class DonaturDashboardController extends GetxController { final now = DateTime.now(); final response = await _supabaseService.client .from('penyaluran_bantuan') - .select() + .select( + '*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)') .order('tanggal_penyaluran', ascending: true); // Konversi ke model lalu filter di sisi client @@ -459,4 +463,25 @@ class DonaturDashboardController extends GetxController { isLoading.value = false; } } + + // Mendapatkan nama lokasi penyaluran berdasarkan ID + Future getLokasiPenyaluran(String? lokasiId) async { + if (lokasiId == null) return null; + + try { + final response = await _supabaseService.client + .from('lokasi_penyaluran') + .select('nama') + .eq('id', lokasiId) + .single(); + + if (response != null && response['nama'] != null) { + return response['nama'] as String; + } + return null; + } catch (e) { + print('Error fetching lokasi penyaluran: $e'); + return null; + } + } } diff --git a/lib/app/modules/donatur/views/donatur_jadwal_detail_view.dart b/lib/app/modules/donatur/views/donatur_jadwal_detail_view.dart new file mode 100644 index 0000000..d4c43a0 --- /dev/null +++ b/lib/app/modules/donatur/views/donatur_jadwal_detail_view.dart @@ -0,0 +1,578 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; + +class DonaturJadwalDetailView extends GetView { + const DonaturJadwalDetailView({Key? key}) : super(key: key); + + @override + DonaturDashboardController get controller { + if (!Get.isRegistered( + tag: 'donatur_dashboard')) { + return Get.put(DonaturDashboardController(), + tag: 'donatur_dashboard', permanent: true); + } + return Get.find(tag: 'donatur_dashboard'); + } + + @override + Widget build(BuildContext context) { + final jadwal = Get.arguments as PenyaluranBantuanModel; + + return Scaffold( + appBar: AppBar( + title: const Text('Detail Jadwal Penyaluran'), + elevation: 0, + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderSection(jadwal), + _buildDetailSection(jadwal), + _buildPelaksanaSection(jadwal), + _buildStatusSection(jadwal), + _buildActionSection(jadwal), + ], + ), + ), + ); + } + + Widget _buildHeaderSection(PenyaluranBantuanModel jadwal) { + String statusText = 'Akan Datang'; + Color statusColor = Colors.blue; + + switch (jadwal.status) { + case 'SELESAI': + statusText = 'Selesai'; + statusColor = Colors.green; + break; + case 'DIBATALKAN': + statusText = 'Dibatalkan'; + statusColor = Colors.red; + break; + case 'DALAM_PROSES': + statusText = 'Dalam Proses'; + statusColor = Colors.orange; + break; + default: + statusText = 'Akan Datang'; + statusColor = Colors.blue; + } + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + statusColor.withOpacity(0.8), + statusColor.withOpacity(0.5), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getStatusIcon(jadwal.status), + size: 16, + color: statusColor, + ), + const SizedBox(width: 6), + Text( + statusText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Text( + jadwal.nama ?? 'Penyaluran Bantuan', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + if (jadwal.tanggalPenyaluran != null) + Row( + children: [ + const Icon( + Icons.calendar_today, + size: 16, + color: Colors.white, + ), + const SizedBox(width: 8), + Text( + DateFormat('EEEE, dd MMMM yyyy', 'id_ID') + .format(jadwal.tanggalPenyaluran!), + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDetailSection(PenyaluranBantuanModel jadwal) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Informasi Penyaluran'), + const SizedBox(height: 16), + _buildInfoItem( + icon: Icons.description_outlined, + title: 'Deskripsi', + value: jadwal.deskripsi ?? 'Tidak ada deskripsi', + ), + const SizedBox(height: 12), + _buildInfoItem( + icon: Icons.people_outline, + title: 'Jumlah Penerima', + value: jadwal.jumlahPenerima != null + ? '${jadwal.jumlahPenerima} orang' + : 'Belum ditentukan', + ), + const SizedBox(height: 12), + _buildInfoItem( + icon: Icons.category_outlined, + title: 'Kategori Bantuan', + value: jadwal.kategoriNama ?? jadwal.kategoriBantuanId ?? 'Umum', + ), + const SizedBox(height: 12), + _buildInfoItem( + icon: Icons.location_on_outlined, + title: 'Lokasi Penyaluran', + value: jadwal.lokasiNama ?? + jadwal.lokasiPenyaluranId ?? + 'Belum ditentukan', + ), + ], + ), + ); + } + + Widget _buildPelaksanaSection(PenyaluranBantuanModel jadwal) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Informasi Pelaksana'), + const SizedBox(height: 16), + Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.grey.shade300), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + CircleAvatar( + radius: 25, + backgroundColor: Colors.blue.shade100, + child: Icon( + Icons.person, + color: Colors.blue.shade700, + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Petugas Pelaksana', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + jadwal.namaPetugas ?? + jadwal.petugasId ?? + 'Belum ditugaskan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildStatusSection(PenyaluranBantuanModel jadwal) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Status Penyaluran'), + const SizedBox(height: 16), + _buildStatusTimeline(jadwal), + ], + ), + ); + } + + Widget _buildStatusTimeline(PenyaluranBantuanModel jadwal) { + final status = jadwal.status; + final bool isCompleted = status == 'SELESAI'; + final bool isCancelled = status == 'DIBATALKAN'; + final bool isInProgress = status == 'DALAM_PROSES'; + + return Column( + children: [ + _buildTimelineItem( + title: 'Dijadwalkan', + date: jadwal.createdAt != null + ? DateFormat('dd MMM yyyy', 'id_ID').format(jadwal.createdAt!) + : '-', + isCompleted: true, + isFirst: true, + ), + _buildTimelineItem( + title: 'Dalam Proses', + date: isInProgress || isCompleted + ? jadwal.tanggalPenyaluran != null + ? DateFormat('dd MMM yyyy', 'id_ID') + .format(jadwal.tanggalPenyaluran!) + : '-' + : '-', + isCompleted: isInProgress || isCompleted, + isCancelled: isCancelled, + ), + _buildTimelineItem( + title: 'Selesai', + date: isCompleted + ? jadwal.tanggalSelesai != null + ? DateFormat('dd MMM yyyy', 'id_ID') + .format(jadwal.tanggalSelesai!) + : '-' + : '-', + isCompleted: isCompleted, + isCancelled: isCancelled, + isLast: true, + ), + if (isCancelled) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.cancel, color: Colors.red.shade700, size: 20), + const SizedBox(width: 8), + Text( + 'Penyaluran Dibatalkan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.red.shade700, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Alasan pembatalan: ${jadwal.alasanPembatalan ?? 'Tidak ada alasan yang diberikan'}', + style: TextStyle(color: Colors.red.shade700), + ), + const SizedBox(height: 8), + if (jadwal.tanggalPembatalan != null) + Text( + 'Dibatalkan pada: ${DateFormat('dd MMMM yyyy', 'id_ID').format(jadwal.tanggalPembatalan!)}', + style: TextStyle( + fontSize: 14, + color: Colors.red.shade700, + ), + ), + ], + ), + ), + ], + ], + ); + } + + Widget _buildTimelineItem({ + required String title, + required String date, + required bool isCompleted, + bool isFirst = false, + bool isLast = false, + bool isCancelled = false, + }) { + return Row( + children: [ + SizedBox( + width: 20, + child: Column( + children: [ + if (!isFirst) + Container( + width: 2, + height: 20, + color: isCompleted + ? Colors.green + : isCancelled + ? Colors.red + : Colors.grey.shade300, + ), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isCompleted + ? Colors.green + : isCancelled + ? Colors.red + : Colors.grey.shade300, + border: Border.all( + color: isCompleted + ? Colors.green + : isCancelled + ? Colors.red + : Colors.grey.shade300, + width: 2, + ), + ), + child: isCompleted + ? const Icon(Icons.check, size: 12, color: Colors.white) + : isCancelled + ? const Icon(Icons.close, size: 12, color: Colors.white) + : null, + ), + if (!isLast) + Container( + width: 2, + height: 20, + color: isCompleted && !isCancelled + ? Colors.green + : Colors.grey.shade300, + ), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container( + margin: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isCompleted + ? Colors.black + : isCancelled + ? Colors.red + : Colors.grey, + ), + ), + Text( + date, + style: TextStyle( + fontSize: 14, + color: isCompleted + ? Colors.grey.shade700 + : isCancelled + ? Colors.red.shade300 + : Colors.grey.shade400, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildActionSection(PenyaluranBantuanModel jadwal) { + if (jadwal.status == 'DIBATALKAN') { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader(title: 'Tindakan'), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _hubungiPetugas(jadwal), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.chat_outlined), + label: const Text('Hubungi Petugas'), + ), + ), + if (jadwal.status == 'SELESAI') ...[ + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: () => _lihatLaporan(jadwal), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + foregroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.description_outlined), + label: const Text('Lihat Laporan'), + ), + ), + ], + ], + ), + ], + ), + ); + } + + Widget _buildInfoItem({ + required IconData icon, + required String title, + required String value, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 20, + color: Colors.blue.shade700, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ); + } + + IconData _getStatusIcon(String? status) { + switch (status) { + case 'SELESAI': + return Icons.check_circle; + case 'DIBATALKAN': + return Icons.cancel; + case 'DALAM_PROSES': + return Icons.timelapse; + default: + return Icons.event_available; + } + } + + void _hubungiPetugas(PenyaluranBantuanModel jadwal) { + // Implementasi untuk menghubungi petugas + Get.snackbar( + 'Fitur Belum Tersedia', + 'Fitur untuk menghubungi petugas akan segera hadir', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.blue.withOpacity(0.9), + colorText: Colors.white, + ); + } + + void _lihatLaporan(PenyaluranBantuanModel jadwal) { + // Navigasi ke halaman laporan + Get.toNamed('/donatur/laporan/${jadwal.id}', arguments: jadwal); + } +} diff --git a/lib/app/modules/donatur/views/donatur_jadwal_view.dart b/lib/app/modules/donatur/views/donatur_jadwal_view.dart index 94720b0..fbb4cc1 100644 --- a/lib/app/modules/donatur/views/donatur_jadwal_view.dart +++ b/lib/app/modules/donatur/views/donatur_jadwal_view.dart @@ -183,140 +183,294 @@ class DonaturJadwalView extends GetView { statusColor = Colors.blue; } - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + return GestureDetector( + onTap: () => _navigateToDetail(jadwal), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 50, - height: 50, - alignment: Alignment.center, - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (jadwal.tanggalPenyaluran != null) ...[ - Text( - DateFormat('dd').format(jadwal.tanggalPenyaluran!), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: statusColor, - ), - ), - Text( - DateFormat('MMM', 'id_ID') - .format(jadwal.tanggalPenyaluran!) - .toUpperCase(), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: statusColor, - ), - ), - ] else - Icon( - Icons.event, - color: statusColor, - size: 24, - ), - ], - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - jadwal.nama ?? 'Penyaluran Bantuan', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + child: Card( + elevation: 0, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: statusColor.withOpacity(0.3), + width: 1.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tanggal dalam badge khusus + Container( + width: 58, + height: 65, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: statusColor.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ) + ], + border: Border.all( + color: statusColor.withOpacity(0.5), + width: 1.5, ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 14, - color: Colors.grey.shade600, - ), - const SizedBox(width: 4), - Text( - formattedDate, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade700, + ), + child: Column( + children: [ + // Header badge + Container( + padding: const EdgeInsets.symmetric(vertical: 3), + width: double.infinity, + decoration: BoxDecoration( + color: statusColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(10), ), ), - ], - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - statusText, - style: TextStyle( - fontSize: 12, - color: statusColor, - fontWeight: FontWeight.bold, + child: Text( + jadwal.tanggalPenyaluran != null + ? DateFormat('MMM', 'id_ID') + .format(jadwal.tanggalPenyaluran!) + .toUpperCase() + : 'TBD', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, ), ), - ), - ], + // Tanggal + Expanded( + child: Center( + child: Text( + jadwal.tanggalPenyaluran != null + ? DateFormat('dd') + .format(jadwal.tanggalPenyaluran!) + : '-', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ), + ), + ], + ), ), + const SizedBox(width: 16), + // Informasi utama + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + jadwal.nama ?? 'Penyaluran Bantuan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + formattedDate, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 4), + if (jadwal.lokasiPenyaluranId != null) ...[ + Row( + children: [ + Icon( + Icons.location_on_outlined, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + jadwal.lokasiNama ?? + jadwal.lokasiPenyaluranId!, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 4), + ], + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getStatusIcon(jadwal.status), + size: 14, + color: statusColor, + ), + const SizedBox(width: 4), + Text( + statusText, + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + if (jadwal.deskripsi != null && + jadwal.deskripsi!.isNotEmpty) ...[ + const Divider(height: 24), + Text( + jadwal.deskripsi!, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ], - ), - if (jadwal.deskripsi != null && jadwal.deskripsi!.isNotEmpty) ...[ - const Divider(height: 24), - Text( - jadwal.deskripsi!, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, + // Footer dengan informasi bantuan dan tombol detail + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Informasi bantuan yang diberikan + if (jadwal.jumlahBantuan != null) ...[ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.volunteer_activism, + size: 14, + color: Colors.blue, + ), + ), + const SizedBox(width: 8), + Text( + '${jadwal.jumlahBantuan} bantuan', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + // Tombol lihat detail + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.grey.shade100, + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Lihat Detail', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.grey.shade700, + ), + ], + ), + ), + ], ), ], - ], + ), ), ), ), ); } + + IconData _getStatusIcon(String? status) { + switch (status) { + case 'SELESAI': + return Icons.check_circle; + case 'DIBATALKAN': + return Icons.cancel; + case 'DALAM_PROSES': + return Icons.timelapse; + default: + return Icons.event_available; + } + } + + void _navigateToDetail(dynamic jadwal) { + Get.toNamed('/donatur/jadwal/${jadwal.id}', arguments: jadwal); + } } diff --git a/lib/app/modules/donatur/views/donatur_penitipan_view.dart b/lib/app/modules/donatur/views/donatur_penitipan_view.dart index cdbbf58..87a2d1d 100644 --- a/lib/app/modules/donatur/views/donatur_penitipan_view.dart +++ b/lib/app/modules/donatur/views/donatur_penitipan_view.dart @@ -46,6 +46,19 @@ class _FormPenitipanBantuanState extends State { super.initState(); // Reset foto bantuan saat form dibuka controller.resetFotoBantuan(); + + // Cek apakah ada skema bantuan yang dipilih dari halaman skema + if (controller.selectedSkemaBantuanId.isNotEmpty) { + // Aktifkan tab skema bantuan + setState(() { + selectedSkemaBantuanId = controller.selectedSkemaBantuanId.value; + }); + + // Reset ID skema setelah digunakan + Future.delayed(Duration.zero, () { + controller.selectedSkemaBantuanId.value = ''; + }); + } } @override @@ -214,6 +227,96 @@ class _FormPenitipanBantuanState extends State { return null; }, ), + const SizedBox(height: 8), + + // Tampilkan informasi stok bantuan dari skema yang dipilih + Obx(() { + // Hanya tampilkan jika skema dipilih + if (selectedSkemaBantuanId == null || + selectedSkemaBantuanId!.isEmpty) { + return const SizedBox.shrink(); + } + + // Cari skema bantuan yang dipilih + SkemaBantuanModel? selectedSkema; + try { + selectedSkema = controller.skemaBantuan.firstWhere( + (skema) => skema.id == selectedSkemaBantuanId, + ); + } catch (_) { + return const SizedBox.shrink(); + } + + // Pastikan skema dan stok bantuan ada + if (selectedSkema.stokBantuanId == null) { + return const SizedBox.shrink(); + } + + // Cari stok bantuan yang sesuai + var stokBantuanFound = false; + var stokNama = 'Tidak diketahui'; + var stokTotal = 0.0; + var stokSatuan = 'item'; + + for (var stok in controller.stokBantuan) { + if (stok.id == selectedSkema.stokBantuanId) { + stokBantuanFound = true; + stokNama = stok.nama ?? 'Tidak diketahui'; + stokTotal = stok.totalStok ?? 0; + stokSatuan = stok.satuan ?? 'item'; + break; + } + } + + if (!stokBantuanFound) { + return const SizedBox.shrink(); + } + + // Tampilkan informasi stok bantuan + return Column( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info_outline, + color: Colors.blue.shade700, size: 18), + const SizedBox(width: 8), + Text( + 'Informasi Stok Bantuan', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Jenis Bantuan: $stokNama', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 4), + Text( + 'Stok Tersedia: $stokTotal $stokSatuan', + style: const TextStyle(fontSize: 14), + ), + ], + ), + ), + const SizedBox(height: 16) + ], + ); + }), + const SizedBox(height: 16), ] else ...[ // Form untuk bantuan manual diff --git a/lib/app/modules/donatur/views/donatur_skema_view.dart b/lib/app/modules/donatur/views/donatur_skema_view.dart index 82e5d52..9487b5c 100644 --- a/lib/app/modules/donatur/views/donatur_skema_view.dart +++ b/lib/app/modules/donatur/views/donatur_skema_view.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart'; +import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; +import 'package:penyaluran_app/app/utils/date_helper.dart'; class DonaturSkemaView extends GetView { const DonaturSkemaView({super.key}); @@ -98,7 +101,7 @@ class DonaturSkemaView extends GetView { color: Colors.grey.shade600, ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), ...controller.skemaBantuan.map((skema) => _buildSkemaCard(skema)), ], ); @@ -106,43 +109,56 @@ class DonaturSkemaView extends GetView { Widget _buildSkemaCard(dynamic skema) { return Container( - margin: const EdgeInsets.only(bottom: 16), + margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.blue.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), + color: Colors.blue.withOpacity(0.1), + blurRadius: 12, + offset: const Offset(0, 5), + spreadRadius: 1, ), ], ), child: Card( elevation: 0, + clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: Colors.blue.shade100, + width: 1, + ), ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan warna gradient + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade600, Colors.blue.shade800], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Row( children: [ Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(12), + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), ), - child: Icon( + child: const Icon( Icons.volunteer_activism, - color: Colors.blue.shade700, - size: 24, + color: Colors.white, + size: 22, ), ), - const SizedBox(width: 16), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -152,23 +168,28 @@ class DonaturSkemaView extends GetView { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: Colors.white, ), ), - const SizedBox(height: 4), - if (skema.kuota != null) - _buildInfoChip( - icon: Icons.people_outline, - label: 'Kuota: ${skema.kuota} penerima', - color: Colors.blue.shade700, - ), - if (skema.jumlahDiterimaPerOrang != null) + if (skema.batasWaktu != null) Padding( padding: const EdgeInsets.only(top: 4), - child: _buildInfoChip( - icon: Icons.inventory_2_outlined, - label: - 'Jumlah per orang: ${skema.jumlahDiterimaPerOrang}', - color: Colors.green.shade700, + child: Row( + children: [ + const Icon( + Icons.timer_outlined, + color: Colors.white70, + size: 14, + ), + const SizedBox(width: 4), + Text( + _formatBatasWaktu(skema.batasWaktu), + style: const TextStyle( + fontSize: 12, + color: Colors.white70, + ), + ), + ], ), ), ], @@ -176,76 +197,320 @@ class DonaturSkemaView extends GetView { ), ], ), - const SizedBox(height: 16), - if (skema.deskripsi != null && skema.deskripsi!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Deskripsi', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, + ), + // Konten utama + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status batas waktu jika hampir habis (kurang dari 7 hari) + if (skema.batasWaktu != null && + _isDeadlineApproaching(skema.batasWaktu)) + Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.orange.shade700, + size: 18, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Segera titipkan bantuan Anda! Batas waktu akan segera berakhir.', + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade800, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ), - const SizedBox(height: 4), - Text( - skema.deskripsi!, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - ), - ), - const Divider(height: 24), - ], - ), - if (skema.kriteria != null && skema.kriteria!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Kriteria Penerima', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.grey.shade800, - ), - ), - const SizedBox(height: 4), - Text( - skema.kriteria!, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - ), - ), - const SizedBox(height: 16), - ], - ), - ElevatedButton.icon( - onPressed: () { - // Navigasi ke formulir penitipan bantuan - controller.activeTabIndex.value = 3; - }, - icon: const Icon(Icons.add_box_outlined, size: 18), - label: const Text('Titipkan Bantuan'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.green.shade600, - minimumSize: const Size(double.infinity, 45), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + + // Tambahkan informasi bantuan yang dibutuhkan + if (skema.stokBantuanId != null) + _buildBantuanDibutuhkan(skema.stokBantuanId!), + + // Informasi kuota dan jumlah per orang + Row( + children: [ + if (skema.kuota != null) + Expanded( + child: _buildInfoBox( + icon: Icons.people_outline, + title: 'Kuota', + value: '${skema.kuota} penerima', + color: Colors.blue.shade700, + ), + ), + const SizedBox(width: 12), + if (skema.jumlahDiterimaPerOrang != null) + Expanded( + child: _buildInfoBox( + icon: Icons.inventory_2_outlined, + title: 'Bantuan Per Orang', + value: _formatJumlahPerOrang(skema.stokBantuanId, + skema.jumlahDiterimaPerOrang!), + color: Colors.green.shade700, + ), + ), + ], ), + + const SizedBox(height: 16), + // Tombol titipkan bantuan + ElevatedButton.icon( + onPressed: () { + // Menyimpan ID skema bantuan yang dipilih + if (skema.id != null) { + controller.selectedSkemaBantuanId.value = skema.id!; + } + // Beralih ke tab penitipan bantuan + controller.activeTabIndex.value = 3; + }, + icon: const Icon(Icons.add_box_outlined, size: 18), + label: const Text('Titipkan Bantuan'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.green.shade600, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildBantuanDibutuhkan(String stokBantuanId) { + // Cari stok bantuan yang sesuai dengan ID di skema + final stokBantuan = controller.stokBantuan.firstWhere( + (stok) => stok.id == stokBantuanId, + orElse: () => StokBantuanModel(), + ); + + if (stokBantuan.id == null) return const SizedBox.shrink(); + + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.inventory_2, + color: Colors.green.shade700, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Bantuan yang Dibutuhkan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green.shade800, ), ), ], ), - ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stokBantuan.nama ?? 'Tidak tersedia', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + if (stokBantuan.kategoriBantuan != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Kategori: ${stokBantuan.kategoriBantuan!['nama'] ?? 'Umum'}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Icon( + Icons.inventory_rounded, + size: 16, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + stokBantuan.isUang == true + ? 'Stok: ${_formatRupiah(stokBantuan.totalStok)}' + : 'Stok: ${stokBantuan.totalStok?.toStringAsFixed(0) ?? '0'} ${stokBantuan.satuan ?? ''}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.green.shade800, + ), + ), + ], + ), + ), + ], + ), + if (stokBantuan.deskripsi != null && + stokBantuan.deskripsi!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + stokBantuan.deskripsi!, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ), + ], ), ); } + Widget _buildInfoBox({ + required IconData icon, + required String title, + required String value, + required Color color, + }) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + size: 16, + color: color, + ), + const SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + value, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _buildInfoSection({ + required String title, + required String content, + required IconData icon, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + size: 16, + color: Colors.grey.shade700, + ), + const SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade200), + ), + child: Text( + content, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + height: 1.4, + ), + ), + ), + ], + ); + } + Widget _buildInfoChip({ required IconData icon, required String label, @@ -270,4 +535,87 @@ class DonaturSkemaView extends GetView { ], ); } + + // Fungsi untuk memformat tanggal batas waktu + String _formatBatasWaktu(DateTime? batasWaktu) { + if (batasWaktu == null) return ''; + + DateTime now = DateTime.now(); + Duration difference = batasWaktu.difference(now); + + if (difference.isNegative) { + return 'Batas waktu telah berakhir'; + } + + int days = difference.inDays; + if (days > 0) { + return 'Batas waktu: ${days} hari lagi'; + } else { + int hours = difference.inHours; + if (hours > 0) { + return 'Batas waktu: ${hours} jam lagi'; + } else { + int minutes = difference.inMinutes; + return 'Batas waktu: ${minutes} menit lagi'; + } + } + } + + // Cek apakah deadline mendekati (kurang dari 7 hari) + bool _isDeadlineApproaching(DateTime? batasWaktu) { + if (batasWaktu == null) return false; + + DateTime now = DateTime.now(); + Duration difference = batasWaktu.difference(now); + + return difference.inDays < 7 && !difference.isNegative; + } + + String _formatJumlahPerOrang( + String? stokBantuanId, dynamic jumlahDiterimaPerOrang) { + // Jika stokBantuanId null, kembalikan nilai apa adanya + if (stokBantuanId == null) return jumlahDiterimaPerOrang.toString(); + + // Cari stok bantuan berdasarkan ID + final stokBantuan = controller.stokBantuan.firstWhere( + (stok) => stok.id == stokBantuanId, + orElse: () => StokBantuanModel(), + ); + + // Jika nilai bantuan berupa uang, format sebagai Rupiah + if (stokBantuan.isUang == true) { + double nilai = 0; + if (jumlahDiterimaPerOrang is int) { + nilai = jumlahDiterimaPerOrang.toDouble(); + } else if (jumlahDiterimaPerOrang is double) { + nilai = jumlahDiterimaPerOrang; + } else { + try { + nilai = double.parse(jumlahDiterimaPerOrang.toString()); + } catch (e) { + return jumlahDiterimaPerOrang.toString(); + } + } + // Format nilai sebagai Rupiah menggunakan DateHelper + return DateHelper.formatRupiah(nilai); + } + + // Jika bukan uang, kembalikan nilai + satuan (jika ada) + return '${jumlahDiterimaPerOrang} ${stokBantuan.satuan ?? ''}'; + } + + String _formatRupiah(dynamic amount) { + if (amount is num) { + return DateHelper.formatRupiah(amount); + } else if (amount is String) { + try { + double nilai = double.parse(amount); + return DateHelper.formatRupiah(nilai); + } catch (e) { + return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}'; + } + } else { + return 'Rp 0'; + } + } } diff --git a/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart b/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart index d8fed51..6b3b4c0 100644 --- a/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart +++ b/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart @@ -13,7 +13,7 @@ import 'package:open_file/open_file.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:http/http.dart' as http; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class LaporanPenyaluranController extends GetxController { final AuthController _authController = Get.find(); diff --git a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart index b7d5c27..7f938e0 100644 --- a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/widgets/custom_app_bar.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'dart:io'; @@ -370,6 +370,9 @@ class LaporanPenyaluranCreateView extends GetView { return; } controller.saveLaporan(penyaluranId); + //kembali reload halaman + // Kembali dan reload halaman setelah menyimpan laporan + Get.back(result: true); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, diff --git a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart index 4695d35..1670333 100644 --- a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/widgets/custom_app_bar.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart'; diff --git a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart index 73c9206..f31c874 100644 --- a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart @@ -3,9 +3,8 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart'; import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/widgets/custom_app_bar.dart'; -import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart'; import 'package:intl/intl.dart'; @@ -34,36 +33,58 @@ class LaporanPenyaluranView extends GetView { if (controller.daftarLaporan.isEmpty) { return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.note_alt_outlined, - size: 64, color: Colors.grey), - const SizedBox(height: 16), - const Text( - 'Belum ada laporan penyaluran', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.description_outlined, + size: 72, + color: AppTheme.primaryColor, + ), ), - ), - const SizedBox(height: 8), - const Text( - 'Buat laporan baru untuk penyaluran yang telah selesai', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () => _showPenyaluranDialog(context), - icon: const Icon(Icons.add), - label: const Text('Buat Laporan Baru'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 12), + const SizedBox(height: 24), + const Text( + 'Belum Ada Laporan Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - ), - ], + const SizedBox(height: 12), + const Text( + 'Buat laporan baru untuk penyaluran bantuan yang telah selesai', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey, + height: 1.5, + ), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => _showPenyaluranDialog(context), + icon: const Icon(Icons.add), + label: const Text('Buat Laporan Baru'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), ), ); } @@ -110,24 +131,42 @@ class LaporanPenyaluranView extends GetView { // Widget untuk filter status Widget _buildStatusFilter() { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - color: AppTheme.primaryColor.withOpacity(0.05), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 2), + ), + ], + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionHeader( - title: 'Filter Status', - // subtitle: 'Tampilkan laporan berdasarkan status', - // showDivider: false, + Row( + children: [ + Icon(Icons.filter_list, color: AppTheme.primaryColor), + const SizedBox(width: 8), + const Text( + 'Filter Status Laporan', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], ), - const SizedBox(height: 8), + const SizedBox(height: 12), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ - _buildFilterChip('SEMUA'), - _buildFilterChip('DRAFT'), - _buildFilterChip('FINAL'), + _buildFilterChip('SEMUA', Icons.list_alt), + _buildFilterChip('DRAFT', Icons.edit_note), + _buildFilterChip('FINAL', Icons.check_circle), ], ), ), @@ -137,23 +176,42 @@ class LaporanPenyaluranView extends GetView { } // Chip untuk filter - Widget _buildFilterChip(String status) { + Widget _buildFilterChip(String status, IconData icon) { return Obx(() { final isSelected = controller.filterStatus.value == status; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: FilterChip( - selected: isSelected, - label: Text(status), - onSelected: (_) { - controller.filterStatus.value = status; - }, - backgroundColor: Colors.white, - checkmarkColor: Colors.white, - selectedColor: AppTheme.primaryColor, - labelStyle: TextStyle( - color: isSelected ? Colors.white : Colors.black, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + return Container( + margin: const EdgeInsets.only(right: 12), + child: Material( + elevation: isSelected ? 2 : 0, + borderRadius: BorderRadius.circular(25), + color: isSelected ? AppTheme.primaryColor : Colors.grey.shade100, + child: InkWell( + onTap: () { + controller.filterStatus.value = status; + }, + borderRadius: BorderRadius.circular(25), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Icon( + icon, + size: 16, + color: isSelected ? Colors.white : Colors.black54, + ), + const SizedBox(width: 6), + Text( + status, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + fontSize: 13, + ), + ), + ], + ), + ), ), ), ); @@ -165,71 +223,77 @@ class LaporanPenyaluranView extends GetView { BuildContext context, LaporanPenyaluranModel laporan) { return Card( margin: const EdgeInsets.only(bottom: 16), - elevation: 3, + elevation: 2, + clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), child: InkWell( onTap: () { Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.id); }, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - gradient: AppTheme.primaryGradient, - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - laporan.judul, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Colors.white, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - StatusBadge(status: laporan.status ?? 'DRAFT'), - ], - ), - const Divider(height: 24, color: Colors.white30), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 16, - color: Colors.white, - ), - const SizedBox(width: 6), - Text( - 'Tanggal: ${laporan.tanggalLaporan != null ? DateTimeHelper.formatDateTime(laporan.tanggalLaporan!) : '-'}', - style: TextStyle( - fontSize: 12, + child: Column( + children: [ + // Header dengan status + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + laporan.judul, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, color: Colors.white, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (laporan.status == 'FINAL') - Material( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), + ), + const SizedBox(width: 8), + StatusBadge(status: laporan.status ?? 'DRAFT'), + ], + ), + ), + // Body with details + Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Informasi dengan icon + Row( + children: [ + _buildInfoItem( + Icons.calendar_today, + 'Tanggal', + laporan.tanggalLaporan != null + ? DateTimeHelper.formatDateTime( + laporan.tanggalLaporan!) + : '-', + ), + _buildInfoItem( + Icons.description, + 'Status', + laporan.status ?? 'DRAFT', + ), + ], + ), + const SizedBox(height: 16), + // Action buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (laporan.status == 'FINAL') + _buildActionButton( + icon: Icons.picture_as_pdf, + label: 'Ekspor PDF', + color: Colors.blue, onTap: () { controller .fetchPenyaluranDetail( @@ -241,85 +305,104 @@ class LaporanPenyaluranView extends GetView { } }); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - child: Row( - children: const [ - Icon( - Icons.picture_as_pdf, - size: 16, - color: Colors.white, - ), - SizedBox(width: 4), - Text( - 'Ekspor PDF', - style: TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), ), - ), - const SizedBox(width: 8), - Material( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 8), + _buildActionButton( + icon: Icons.edit, + label: 'Edit', + color: Colors.orange, onTap: () { Get.toNamed('/laporan-penyaluran/edit', arguments: laporan.id); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - child: Row( - children: const [ - Icon(Icons.edit, color: Colors.orange, size: 16), - SizedBox(width: 4), - Text('Edit', - style: TextStyle( - color: Colors.orange, - fontWeight: FontWeight.bold)), - ], - ), - ), ), - ), - const SizedBox(width: 8), - Material( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 8), + _buildActionButton( + icon: Icons.delete, + label: 'Hapus', + color: Colors.red, onTap: () { _showDeleteConfirmation(context, laporan.id!); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - child: Row( - children: const [ - Icon(Icons.delete, color: Colors.red, size: 16), - SizedBox(width: 4), - Text('Hapus', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold)), - ], - ), - ), ), - ), - ], + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + // Helper untuk item informasi + Widget _buildInfoItem(IconData icon, String label, String value) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: AppTheme.primaryColor), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), ), ], ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ], + ), + ), + ); + } + + // Helper untuk tombol aksi + Widget _buildActionButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return Material( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Row( + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], ), ), ), @@ -332,19 +415,42 @@ class LaporanPenyaluranView extends GetView { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Hapus Laporan'), - content: const Text('Apakah Anda yakin ingin menghapus laporan ini?'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.red, size: 24), + const SizedBox(width: 8), + const Text('Hapus Laporan'), + ], + ), + content: const Text( + 'Apakah Anda yakin ingin menghapus laporan ini? Tindakan ini tidak dapat dibatalkan.', + style: TextStyle(height: 1.5), + ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Batal'), ), - TextButton( + ElevatedButton( onPressed: () { Navigator.of(context).pop(); - controller.deleteLaporan(laporanId); + controller.deleteLaporan(laporanId).then((_) { + Get.snackbar( + 'Berhasil', + 'Laporan berhasil dihapus', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }); }, - style: TextButton.styleFrom(foregroundColor: Colors.red), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), child: const Text('Hapus'), ), ], @@ -370,31 +476,90 @@ class LaporanPenyaluranView extends GetView { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Pilih Penyaluran'), + title: Row( + children: [ + Icon(Icons.assignment, color: AppTheme.primaryColor), + const SizedBox(width: 8), + const Text('Pilih Penyaluran'), + ], + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: const EdgeInsets.only(top: 16, left: 24, right: 24), content: SizedBox( width: double.maxFinite, - child: ListView.builder( - shrinkWrap: true, - itemCount: controller.penyaluranTanpaLaporan.length, - itemBuilder: (context, index) { - final penyaluran = controller.penyaluranTanpaLaporan[index]; - return ListTile( - title: Text( - penyaluran.nama ?? - 'Penyaluran #${penyaluran.id?.substring(0, 8)}', - style: const TextStyle(fontWeight: FontWeight.bold), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pilih salah satu penyaluran bantuan yang akan dibuat laporannya:', + style: TextStyle(color: Colors.grey.shade700, fontSize: 13), + ), + const SizedBox(height: 8), + Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.4, ), - subtitle: Text( - 'Tanggal: ${penyaluran.tanggalSelesai != null ? DateFormat('dd/MM/yyyy').format(penyaluran.tanggalSelesai!) : '-'}', + child: ListView.separated( + shrinkWrap: true, + itemCount: controller.penyaluranTanpaLaporan.length, + separatorBuilder: (context, index) => + const Divider(height: 1), + itemBuilder: (context, index) { + final penyaluran = + controller.penyaluranTanpaLaporan[index]; + return ListTile( + contentPadding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 8, + ), + leading: CircleAvatar( + backgroundColor: + AppTheme.primaryColor.withOpacity(0.2), + child: const Icon( + Icons.inventory_2, + color: AppTheme.primaryColor, + ), + ), + title: Text( + penyaluran.nama ?? + 'Penyaluran #${penyaluran.id?.substring(0, 8)}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Row( + children: [ + Icon( + Icons.calendar_today, + size: 12, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Text( + penyaluran.tanggalSelesai != null + ? DateFormat('dd/MM/yyyy') + .format(penyaluran.tanggalSelesai!) + : '-', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + Navigator.of(context).pop(); + // Arahkan ke halaman buat laporan dengan ID penyaluran + Get.toNamed('/laporan-penyaluran/create', + arguments: penyaluran.id); + }, + ); + }, ), - onTap: () { - Navigator.of(context).pop(); - // Arahkan ke halaman buat laporan dengan ID penyaluran - Get.toNamed('/laporan-penyaluran/create', - arguments: penyaluran.id); - }, - ); - }, + ), + ], ), ), actions: [ diff --git a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart index 903a07e..9cef6c9 100644 --- a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart +++ b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart @@ -4,7 +4,7 @@ import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.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/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class CalendarViewWidget extends StatelessWidget { final JadwalPenyaluranController controller; @@ -25,30 +25,48 @@ class CalendarViewWidget extends StatelessWidget { width: double.infinity, decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 1), + blurRadius: 5, + offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( + Container( padding: const EdgeInsets.all(16.0), - child: Text( - 'Kalender Penyaluran Bulan Ini', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + decoration: BoxDecoration( + gradient: AppTheme.primaryGradient, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + const Icon( + Icons.calendar_month_rounded, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'Kalender Penyaluran', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], ), ), Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(12.0), child: SizedBox( height: MediaQuery.of(context).size.height * 0.65, child: Obx(() { @@ -64,7 +82,7 @@ class CalendarViewWidget extends StatelessWidget { MonthAppointmentDisplayMode.indicator, showAgenda: true, agendaViewHeight: MediaQuery.of(context).size.height * 0.3, - agendaItemHeight: 60, + agendaItemHeight: 70, dayFormat: 'EEE', numberOfWeeksInView: 6, appointmentDisplayCount: 3, @@ -82,19 +100,19 @@ class CalendarViewWidget extends StatelessWidget { color: Colors.grey.withOpacity(0.7), ), ), - agendaStyle: const AgendaStyle( - backgroundColor: Colors.white, - appointmentTextStyle: TextStyle( + agendaStyle: AgendaStyle( + backgroundColor: Colors.grey.shade50, + appointmentTextStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white, ), - dateTextStyle: TextStyle( + dateTextStyle: const TextStyle( fontSize: 14, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, color: AppTheme.primaryColor, ), - dayTextStyle: TextStyle( + dayTextStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppTheme.primaryColor, @@ -106,7 +124,7 @@ class CalendarViewWidget extends StatelessWidget { selectionDecoration: BoxDecoration( color: Colors.transparent, border: Border.all(color: AppTheme.primaryColor, width: 2), - borderRadius: const BorderRadius.all(Radius.circular(4)), + borderRadius: const BorderRadius.all(Radius.circular(8)), shape: BoxShape.rectangle, ), headerStyle: const CalendarHeaderStyle( @@ -163,6 +181,13 @@ class CalendarViewWidget extends StatelessWidget { ? BoxDecoration( shape: BoxShape.circle, color: AppTheme.primaryColor, + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], ) : null, child: Column( @@ -179,7 +204,7 @@ class CalendarViewWidget extends StatelessWidget { details.date.year == DateTime.now().year ? Colors.white : AppTheme.primaryColor, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, ), ), ), @@ -201,12 +226,19 @@ class CalendarViewWidget extends StatelessWidget { ), if (hasAppointments && details.appointments.isEmpty) Container( - width: 6, - height: 6, + width: 8, + height: 8, margin: const EdgeInsets.only(bottom: 2), - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, color: AppTheme.primaryColor, + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + blurRadius: 2, + spreadRadius: 0.5, + ) + ], ), ), ], @@ -322,131 +354,245 @@ class CalendarViewWidget extends StatelessWidget { showModalBottomSheet( context: context, isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), + backgroundColor: Colors.transparent, builder: (context) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 10, + spreadRadius: 0, + offset: Offset(0, -2), + ), + ], + ), constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - appointment.subject, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle bar + Center( + child: Container( + width: 40, + height: 5, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(30), + ), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + appointment.subject, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - if (status != null) _buildStatusBadge(status), - ], + ), + const SizedBox(width: 8), + if (status != null) _buildStatusBadge(status), + ], + ), + + const SizedBox(height: 16), + + // Date, time and location info + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), ), - const SizedBox(height: 10), - if (status != null) ...[ - Row( - children: [ - Icon( - _getStatusIcon(status), - size: 16, - color: _getStatusColor(status), - ), - const SizedBox(width: 8), - Text( - 'Status: ${_getStatusText(status)}', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: _getStatusColor(status), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.calendar_today, + size: 16, + color: AppTheme.primaryColor, + ), ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tanggal', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + formattedDate, + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w600), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.access_time, + size: 16, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Waktu', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + '${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB', + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w600), + ), + ], + ), + ], + ), + if (appointment.location != null && + appointment.location!.isNotEmpty) ...[ + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.location_on, + size: 16, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lokasi', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + appointment.location!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], ), ], - ), - const SizedBox(height: 8), - ], - Row( - children: [ - const Icon(Icons.calendar_today, size: 16), - const SizedBox(width: 8), - Text( - formattedDate, - style: const TextStyle(fontSize: 16), - ), ], ), + ), + + if (appointment.notes != null && + appointment.notes!.isNotEmpty) ...[ + const SizedBox(height: 20), + Text( + 'Deskripsi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), const SizedBox(height: 8), - Row( - children: [ - const Icon(Icons.access_time, size: 16), - const SizedBox(width: 8), - Text( - '${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB', - style: const TextStyle(fontSize: 16), - ), - ], - ), - if (appointment.location != null && - appointment.location!.isNotEmpty) ...[ - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.location_on, size: 16), - const SizedBox(width: 8), - Expanded( - child: Text( - appointment.location!, - style: const TextStyle(fontSize: 16), - ), - ), - ], + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), ), - ], - if (appointment.notes != null && - appointment.notes!.isNotEmpty) ...[ - const SizedBox(height: 16), - const Text( - 'Deskripsi:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( + child: Text( appointment.notes!, - style: const TextStyle(fontSize: 16), - ), - ], - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + height: 1.5, ), - child: const Text('Tutup'), ), ), ], - ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + 'Tutup', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], ), ), ), diff --git a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart index f244283..68537ee 100644 --- a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart +++ b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class JadwalSectionWidget extends StatelessWidget { @@ -43,32 +43,56 @@ class JadwalSectionWidget extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), Obx(() { final currentJadwalList = _getCurrentJadwalList(); if (currentJadwalList.isEmpty) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: Colors.grey.withAlpha(20), - borderRadius: BorderRadius.circular(12), + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Center( child: Column( children: [ - Icon( - _getEmptyIcon(), - size: 40, - color: Colors.grey.shade400, + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon( + _getEmptyIcon(), + size: 48, + color: Colors.grey.shade400, + ), ), - const SizedBox(height: 8), + const SizedBox(height: 16), Text( 'Tidak ada jadwal $title', style: textTheme.titleMedium?.copyWith( color: Colors.grey.shade600, + fontWeight: FontWeight.w500, ), ), + const SizedBox(height: 8), + Text( + 'Jadwal akan muncul di sini saat tersedia', + style: textTheme.bodySmall?.copyWith( + color: Colors.grey.shade500, + ), + textAlign: TextAlign.center, + ), ], ), ), @@ -192,17 +216,18 @@ class JadwalSectionWidget extends StatelessWidget { controller.getKategoriBantuanName(jadwal.kategoriBantuanId); return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, + margin: const EdgeInsets.only(bottom: 16), + elevation: 3, + shadowColor: Colors.black.withOpacity(0.1), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), side: BorderSide( color: statusColor.withOpacity(0.3), - width: 1, + width: 1.5, ), ), child: InkWell( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), onTap: () { if (jadwal.id != null) { Get.toNamed(Routes.detailPenyaluran, @@ -223,19 +248,19 @@ class JadwalSectionWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Container( - // padding: const EdgeInsets.all(10), - // decoration: BoxDecoration( - // color: statusColor.withOpacity(0.1), - // borderRadius: BorderRadius.circular(10), - // ), - // child: Icon( - // _getStatusIcon(), - // color: statusColor, - // size: 24, - // ), - // ), - // const SizedBox(width: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + _getStatusIcon(), + color: statusColor, + size: 24, + ), + ), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -254,12 +279,16 @@ class JadwalSectionWidget extends StatelessWidget { ), Container( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, + horizontal: 10, + vertical: 6, ), decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + color: statusColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: statusColor.withOpacity(0.3), + width: 1, + ), ), child: Text( statusText, @@ -273,10 +302,12 @@ class JadwalSectionWidget extends StatelessWidget { ), if (jadwal.deskripsi != null && jadwal.deskripsi!.isNotEmpty) ...[ - const SizedBox(height: 4), + const SizedBox(height: 6), Text( jadwal.deskripsi!, - style: textTheme.bodyMedium, + style: textTheme.bodyMedium?.copyWith( + color: Colors.grey.shade700, + ), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -287,47 +318,18 @@ class JadwalSectionWidget extends StatelessWidget { ], ), const Divider(height: 24), - _buildInfoItem( - Icons.location_on_outlined, - 'Lokasi', - lokasiName, + _buildInfoSection( textTheme, + lokasiName, + kategoriName, + formattedDateTime, + jadwal.jumlahPenerima, ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: _buildInfoItem( - Icons.category_outlined, - 'Kategori', - kategoriName, - textTheme, - ), - ), - Expanded( - child: _buildInfoItem( - Icons.event, - 'Jadwal', - formattedDateTime, - textTheme, - ), - ), - ], - ), - if (jadwal.jumlahPenerima != null) ...[ - const SizedBox(height: 8), - _buildInfoItem( - Icons.people_outline, - 'Jumlah Penerima', - '${jadwal.jumlahPenerima}', - textTheme, - ), - ], - const SizedBox(height: 16), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton.icon( + ElevatedButton.icon( onPressed: () { if (jadwal.id != null) { Get.toNamed(Routes.detailPenyaluran, @@ -340,10 +342,19 @@ class JadwalSectionWidget extends StatelessWidget { ); } }, - icon: const Icon(Icons.visibility_outlined), + icon: const Icon(Icons.visibility_outlined, size: 18), label: const Text('Lihat Detail'), - style: TextButton.styleFrom( - foregroundColor: statusColor, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: statusColor, + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), ], @@ -355,6 +366,66 @@ class JadwalSectionWidget extends StatelessWidget { ); } + Widget _buildInfoSection( + TextTheme textTheme, + String lokasiName, + String kategoriName, + String formattedDateTime, + int? jumlahPenerima, + ) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + ), + child: Column( + children: [ + _buildInfoItem( + Icons.location_on_outlined, + 'Lokasi', + lokasiName, + textTheme, + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: _buildInfoItem( + Icons.category_outlined, + 'Kategori', + kategoriName, + textTheme, + ), + ), + Expanded( + child: _buildInfoItem( + Icons.event, + 'Jadwal', + formattedDateTime, + textTheme, + ), + ), + ], + ), + if (jumlahPenerima != null) ...[ + const SizedBox(height: 10), + _buildInfoItem( + Icons.people_outline, + 'Jumlah Penerima', + '$jumlahPenerima', + textTheme, + ), + ], + ], + ), + ); + } + Widget _buildInfoItem( IconData icon, String label, @@ -364,12 +435,19 @@ class JadwalSectionWidget extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - icon, - size: 16, - color: Colors.grey.shade600, + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + icon, + size: 14, + color: Colors.grey.shade700, + ), ), - const SizedBox(width: 4), + const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -378,11 +456,15 @@ class JadwalSectionWidget extends StatelessWidget { label, style: textTheme.bodySmall?.copyWith( color: Colors.grey.shade600, + fontWeight: FontWeight.w500, ), ), + const SizedBox(height: 2), Text( value, - style: textTheme.bodyMedium, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), overflow: TextOverflow.ellipsis, maxLines: 1, ), 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 03ec793..aaca91b 100644 --- a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -7,7 +7,7 @@ import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; diff --git a/lib/app/modules/petugas_desa/controllers/pelaksanaan_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/pelaksanaan_penyaluran_controller.dart index e88c041..e2c09a5 100644 --- a/lib/app/modules/petugas_desa/controllers/pelaksanaan_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/pelaksanaan_penyaluran_controller.dart @@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class PelaksanaanPenyaluranController extends GetxController { diff --git a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart index 02aadab..a9064e1 100644 --- a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class PenerimaController extends GetxController { final RxList> daftarPenerima = diff --git a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart index ca45d09..7729bb3 100644 --- a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart @@ -296,6 +296,32 @@ class PengaduanController extends GetxController { } } + Future updateStatusTindakan(String pengaduanId) async { + isLoading.value = true; + try { + await _supabaseService.updateStatusPengaduan(pengaduanId, 'TINDAKAN'); + await loadPengaduanData(); + Get.snackbar( + 'Sukses', + 'Pengaduan berhasil diubah ke status Tindakan', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.blue, + colorText: Colors.white, + ); + } catch (e) { + print('Error updating to Tindakan status: $e'); + Get.snackbar( + 'Error', + 'Gagal mengubah status ke Tindakan: ${e.toString()}', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isLoading.value = false; + } + } + Future> getTindakanPengaduan( String pengaduanId) async { try { diff --git a/lib/app/modules/petugas_desa/views/dashboard_view.dart b/lib/app/modules/petugas_desa/views/dashboard_view.dart index 3d06a02..8160c40 100644 --- a/lib/app/modules/petugas_desa/views/dashboard_view.dart +++ b/lib/app/modules/petugas_desa/views/dashboard_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/greeting_header.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/schedule_card.dart'; diff --git a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart index 097f83b..a502794 100644 --- a/lib/app/modules/petugas_desa/views/detail_donatur_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_donatur_view.dart @@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/donatur_cont import 'package:penyaluran_app/app/data/models/donatur_model.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class DetailDonaturView extends GetView { const DetailDonaturView({super.key}); diff --git a/lib/app/modules/petugas_desa/views/detail_penerima_view.dart b/lib/app/modules/petugas_desa/views/detail_penerima_view.dart index eb8c909..956e5d8 100644 --- a/lib/app/modules/petugas_desa/views/detail_penerima_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_penerima_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class DetailPenerimaView extends GetView { const DetailPenerimaView({super.key}); 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 557504e..7a297f1 100644 --- a/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/detail_pengaduan_view.dart @@ -14,6 +14,8 @@ import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart'; import 'package:penyaluran_app/app/widgets/inputs/text_input.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:penyaluran_app/app/routes/app_pages.dart'; class DetailPengaduanView extends GetView { const DetailPengaduanView({super.key}); @@ -88,34 +90,100 @@ class DetailPengaduanView extends GetView { ), ], ), - body: FutureBuilder>( - future: controller.getDetailPengaduan(pengaduanId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Text('Error: ${snapshot.error}'), - ); - } - - final data = snapshot.data; - if (data == null || data['pengaduan'] == null) { - return const Center( - child: Text('Data pengaduan tidak ditemukan'), - ); - } - - final pengaduan = PengaduanModel.fromJson(data['pengaduan']); - final List tindakanList = - (data['tindakan'] as List) - .map((item) => TindakanPengaduanModel.fromJson(item)) - .toList(); - - return _buildDetailContent(context, pengaduan, tindakanList); + body: RefreshIndicator( + onRefresh: () async { + await controller.getDetailPengaduan(pengaduanId); }, + color: AppTheme.primaryColor, + child: FutureBuilder>( + future: controller.getDetailPengaduan(pengaduanId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(AppTheme.primaryColor), + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 60, + color: Colors.red.shade300, + ), + const SizedBox(height: 16), + Text( + 'Error: ${snapshot.error}', + style: const TextStyle( + fontSize: 16, + color: Colors.red, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + controller.getDetailPengaduan(pengaduanId); + }, + icon: const Icon(Icons.refresh), + label: const Text('Coba Lagi'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + ), + ), + ], + ), + ); + } + + final data = snapshot.data; + if (data == null || data['pengaduan'] == null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.search_off, + size: 60, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Data pengaduan tidak ditemukan', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Text( + 'Pengaduan mungkin telah dihapus atau tidak tersedia', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ); + } + + final pengaduan = PengaduanModel.fromJson(data['pengaduan']); + final List tindakanList = + (data['tindakan'] as List) + .map((item) => TindakanPengaduanModel.fromJson(item)) + .toList(); + + return _buildDetailContent(context, pengaduan, tindakanList); + }, + ), ), floatingActionButton: FutureBuilder>( future: controller.getDetailPengaduan(pengaduanId), @@ -172,38 +240,51 @@ class DetailPengaduanView extends GetView { statusText = pengaduan.status ?? 'Tidak Diketahui'; } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan status - _buildHeaderWithStatus(context, pengaduan, statusColor, statusText), + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan status + _buildHeaderWithStatus(context, pengaduan, statusColor, statusText), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Informasi pengaduan - _buildPengaduanInfo(context, pengaduan), + // Informasi pengaduan + _buildPengaduanInfo(context, pengaduan), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Informasi penyaluran yang diadukan - if (pengaduan.penerimaPenyaluran != null) - _buildPenyaluranInfo(context, pengaduan), + // Informasi penyaluran yang diadukan + if (pengaduan.penerimaPenyaluran != null) + _buildPenyaluranInfo(context, pengaduan), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Feedback warga jika status SELESAI - if (pengaduan.status?.toUpperCase() == 'SELESAI' && - (pengaduan.feedbackWarga != null || - pengaduan.ratingWarga != null)) - _buildFeedbackSection(context, pengaduan), + // Feedback warga jika status SELESAI + if (pengaduan.status?.toUpperCase() == 'SELESAI' && + (pengaduan.feedbackWarga != null || + pengaduan.ratingWarga != null)) + _buildFeedbackSection(context, pengaduan), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Timeline tindakan - _buildTindakanTimeline(context, tindakanList), - ], + // Timeline tindakan + _buildTindakanTimeline(context, tindakanList), + + // Padding di bagian bawah untuk memberikan space saat ada floating action button + const SizedBox(height: 80), + ], + ), ), ); } @@ -238,21 +319,21 @@ class DetailPengaduanView extends GetView { ), child: Column( children: [ - _buildStatusGuideItem( + _buildStatusInfo( 'MENUNGGU', 'Pengaduan baru yang belum ditindaklanjuti', statusMenungguColor, Icons.hourglass_empty, ), const SizedBox(height: 8), - _buildStatusGuideItem( + _buildStatusInfo( 'TINDAKAN', 'Pengaduan sedang dalam proses penanganan', statusTindakanColor, Icons.engineering, ), const SizedBox(height: 8), - _buildStatusGuideItem( + _buildStatusInfo( 'SELESAI', 'Pengaduan telah selesai ditangani', statusSelesaiColor, @@ -305,17 +386,18 @@ class DetailPengaduanView extends GetView { ); } - Widget _buildStatusGuideItem( + Widget _buildStatusInfo( String status, String description, Color color, IconData icon, ) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 32, - height: 32, + width: 36, + height: 36, decoration: BoxDecoration( color: color.withOpacity(0.2), shape: BoxShape.circle, @@ -324,27 +406,29 @@ class DetailPengaduanView extends GetView { child: Icon( icon, color: color, - size: 18, + size: 20, ), ), ), - const SizedBox(width: 12), + const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Menggunakan StatusPill untuk status StatusPill( - status: status, + status: _getStatusText(status), backgroundColor: color, textColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), - const SizedBox(height: 4), + const SizedBox(height: 8), Text( description, style: TextStyle( - fontSize: 12, + fontSize: 14, color: Colors.grey.shade700, + height: 1.4, ), ), ], @@ -354,511 +438,56 @@ class DetailPengaduanView extends GetView { ); } - Widget _buildTindakanStatusItem( - String status, - String label, - String description, - Color color, - IconData icon, - ) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - icon, - color: color, - size: 16, - ), - const SizedBox(width: 8), - // Menggunakan StatusPill untuk status tindakan - StatusPill( - status: label, - backgroundColor: color, - textColor: Colors.white, - ), - ], - ), - const SizedBox(height: 4), - Text( - description, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - ), - ), - ], - ), - ); - } - - Widget _buildHeaderWithStatus( - BuildContext context, - PengaduanModel pengaduan, - Color statusColor, - String statusText, - ) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - pengaduan.judul ?? 'Pengaduan', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - // Menggunakan StatusPill untuk menampilkan status - _getStatusPill(pengaduan.status), - ], - ), - const SizedBox(height: 12), - Text( - pengaduan.deskripsi ?? '', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - ), - ), - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 16, - color: Colors.grey.shade600, - ), - const SizedBox(width: 8), - Text( - pengaduan.tanggalPengaduan != null - ? DateFormat('dd MMMM yyyy', 'id_ID') - .format(pengaduan.tanggalPengaduan!) - : '-', - style: TextStyle( - color: Colors.grey.shade600, - ), - ), - ], - ), - 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), - - // Tampilkan feedback dan rating warga jika ada - if (pengaduan.status?.toUpperCase() == 'SELESAI' && - (pengaduan.feedbackWarga != null || - pengaduan.ratingWarga != null)) - Column( - children: [ - const SizedBox(height: 16), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.amber.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.amber.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Feedback Warga', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.amber, - ), - ), - if (pengaduan.ratingWarga != null) - Row( - children: List.generate(5, (index) { - return Icon( - index < (pengaduan.ratingWarga ?? 0) - ? Icons.star - : Icons.star_border, - color: Colors.amber, - size: 16, - ); - }), - ), - ], - ), - const SizedBox(height: 8), - if (pengaduan.feedbackWarga != null && - pengaduan.feedbackWarga!.isNotEmpty) - Text( - pengaduan.feedbackWarga!, - style: TextStyle( - fontSize: 13, - color: Colors.amber.shade900, - fontStyle: FontStyle.italic, - ), - ) - else - Text( - 'Warga belum memberikan komentar', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - fontStyle: FontStyle.italic, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ); - } - - // Helper method untuk mendapatkan StatusPill berdasarkan status - StatusPill _getStatusPill(String? status) { - switch (status?.toUpperCase()) { + String _getStatusText(String status) { + switch (status) { case 'MENUNGGU': - return StatusPill( - status: 'Menunggu', - backgroundColor: statusMenungguColor, - textColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ); + return 'Menunggu'; case 'TINDAKAN': - return StatusPill( - status: 'Tindakan', - backgroundColor: statusTindakanColor, - textColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ); + return 'Tindakan'; case 'SELESAI': - return StatusPill( - status: 'Selesai', - backgroundColor: statusSelesaiColor, - textColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ); + return 'Selesai'; default: - return StatusPill( - status: status ?? 'Tidak Diketahui', - backgroundColor: Colors.grey, - textColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ); + return status; } } - Widget _buildStatusPanel(BuildContext context, PengaduanModel pengaduan) { - final status = pengaduan.status?.toUpperCase() ?? ''; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Status Pengaduan', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - // Tambahkan tombol untuk mengubah status - if (status != 'SELESAI') - TextButton.icon( - onPressed: () { - _showUbahStatusDialog(context, pengaduan); - }, - icon: const Icon(Icons.edit, size: 16), - label: const Text('Ubah Status'), - style: TextButton.styleFrom( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildStatusStep( - 'MENUNGGU', - 'Menunggu', - status == 'MENUNGGU', - status == 'MENUNGGU' || - status == 'TINDAKAN' || - status == 'SELESAI', - statusMenungguColor, - ), - ), - Expanded( - child: _buildStatusStep( - 'TINDAKAN', - 'Tindakan', - status == 'TINDAKAN', - status == 'TINDAKAN' || status == 'SELESAI', - statusTindakanColor, - ), - ), - Expanded( - child: _buildStatusStep( - 'SELESAI', - 'Selesai', - status == 'SELESAI', - status == 'SELESAI', - statusSelesaiColor, - ), - ), - ], - ), - const SizedBox(height: 16), - // Tombol aksi berdasarkan status - if (status == 'MENUNGGU') - ElevatedButton.icon( - onPressed: () { - _showTambahTindakanDialog(context, pengaduan.id!); - }, - icon: const Icon(Icons.engineering), - label: const Text('Tambah Tindakan'), - style: ElevatedButton.styleFrom( - backgroundColor: statusTindakanColor, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 40), - ), - ) - else if (status == 'TINDAKAN') - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () { - _showTambahTindakanDialog(context, pengaduan.id!); - }, - icon: const Icon(Icons.add), - label: const Text('Tambah Tindakan'), - style: ElevatedButton.styleFrom( - backgroundColor: statusTindakanColor, - foregroundColor: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton.icon( - onPressed: () { - _showKonfirmasiSelesai(context, pengaduan.id!); - }, - icon: const Icon(Icons.check_circle), - label: const Text('Selesaikan'), - style: ElevatedButton.styleFrom( - backgroundColor: statusSelesaiColor, - foregroundColor: Colors.white, - ), - ), - ), - ], - ) - else if (status == 'SELESAI') - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: statusSelesaiColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: statusSelesaiColor.withOpacity(0.3)), - ), - child: Row( - children: [ - Icon( - Icons.check_circle, - color: statusSelesaiColor, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Pengaduan telah selesai ditangani', - style: TextStyle( - color: statusSelesaiColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ], - ), - ); + Color _getStatusColor(String status) { + switch (status) { + case 'MENUNGGU': + return statusMenungguColor; + case 'TINDAKAN': + return statusTindakanColor; + case 'SELESAI': + return statusSelesaiColor; + default: + return Colors.grey; + } } - Widget _buildStatusStep( - String statusValue, - String label, - bool isActive, - bool isCompleted, - Color color, - ) { - return Column( - children: [ - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: isActive - ? color - : (isCompleted ? color.withOpacity(0.3) : Colors.grey.shade300), - shape: BoxShape.circle, - ), - child: Center( - child: isCompleted - ? Icon( - Icons.check, - color: Colors.white, - size: 16, - ) - : Text( - (statusValue == 'MENUNGGU' - ? '1' - : (statusValue == 'TINDAKAN' ? '2' : '3')), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 12, - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - color: isActive ? color : Colors.grey.shade600, - ), - ), - ], - ); + String _getStatusDescription(String status) { + switch (status) { + case 'MENUNGGU': + return 'Pengaduan baru yang belum ditindaklanjuti oleh petugas.'; + case 'TINDAKAN': + return 'Pengaduan sedang dalam proses penanganan oleh petugas.'; + case 'SELESAI': + return 'Pengaduan telah selesai ditangani oleh petugas.'; + default: + return 'Status pengaduan tidak diketahui.'; + } + } + + IconData _getStatusIcon(String status) { + switch (status) { + case 'MENUNGGU': + return Icons.hourglass_empty; + case 'TINDAKAN': + return Icons.engineering; + case 'SELESAI': + return Icons.check_circle; + default: + return Icons.help_outline; + } } void _showKonfirmasiSelesai(BuildContext context, String pengaduanId) async { @@ -944,40 +573,69 @@ class DetailPengaduanView extends GetView { final String noHpWarga = warga != null ? warga['no_hp'] ?? '-' : '-'; return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan judul Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( 'Informasi Pelapor', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + color: Colors.green.shade700, ), ), - Icon( - Icons.person, - color: AppTheme.primaryColor, - ), ], ), - const Divider(height: 24), + const SizedBox(height: 16), // Informasi pelapor - _buildInfoRow('Nama', namaWarga), - _buildInfoRow('NIK', nikWarga), - _buildInfoRow('Alamat', alamatWarga), - _buildInfoRow('No. HP', noHpWarga), + 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: [ + _buildInfoRow('Nama', namaWarga, Icons.person_outline), + _buildInfoRow('NIK', nikWarga, Icons.badge), + _buildInfoRow('Alamat', alamatWarga, Icons.home_outlined), + _buildInfoRow('No. HP', noHpWarga, Icons.phone_outlined), + ], + ), + ), ], ), ), @@ -986,71 +644,120 @@ class DetailPengaduanView extends GetView { Widget _buildPenyaluranInfo(BuildContext context, PengaduanModel pengaduan) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan judul Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.purple.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.inventory, + color: Colors.purple.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( 'Informasi Penyaluran', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + color: Colors.purple.shade700, ), ), - Icon( - Icons.inventory, - color: AppTheme.primaryColor, - ), ], ), - const Divider(height: 24), + const SizedBox(height: 16), // Informasi penyaluran - _buildInfoRow('Nama Penyaluran', pengaduan.namaPenyaluran), - _buildInfoRow('Stok Bantuan', pengaduan.stokBantuan!['nama']), - _buildInfoRow('Jumlah Bantuan', - '${pengaduan.jumlahBantuan} ${pengaduan.stokBantuan!['satuan']}'), - _buildInfoRow('Deskripsi', pengaduan.deskripsiPenyaluran), + 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: [ + _buildInfoRow('Nama Penyaluran', pengaduan.namaPenyaluran, + Icons.label_outline), + _buildInfoRow('Stok Bantuan', pengaduan.stokBantuan!['nama'], + Icons.category_outlined), + _buildInfoRow( + 'Jumlah Bantuan', + '${pengaduan.jumlahBantuan} ${pengaduan.stokBantuan!['satuan']}', + Icons.shopping_bag_outlined), + _buildInfoRow('Deskripsi', pengaduan.deskripsiPenyaluran, + Icons.description_outlined), + ], + ), + ), ], ), ), ); } - Widget _buildInfoRow(String label, String value) { + Widget _buildInfoRow(String label, String? value, IconData icon) { return Padding( - padding: const EdgeInsets.only(bottom: 12.0), + padding: const EdgeInsets.only(bottom: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 120, - child: Text( - label, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 18, + color: AppTheme.primaryColor, ), ), - const SizedBox(width: 8), + const SizedBox(width: 16), Expanded( - child: Text( - value, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + value ?? '-', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ], @@ -1063,46 +770,88 @@ class DetailPengaduanView extends GetView { List tindakanList, ) { if (tindakanList.isEmpty) { - return InfoCard( - title: 'Belum Ada Tindakan', - description: 'Belum ada tindakan untuk pengaduan ini', - icon: Icons.info_outline, - backgroundColor: Colors.grey.shade50, + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + padding: const EdgeInsets.all(20), + child: InfoCard( + title: 'Belum Ada Tindakan', + description: + 'Pengaduan ini belum mendapatkan tindakan dari petugas', + icon: Icons.hourglass_empty, + backgroundColor: Colors.orange.shade50, + iconColor: Colors.orange, + ), + ), ); } return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Menggunakan SectionHeader untuk judul - SectionHeader( - title: 'Riwayat Tindakan', - padding: EdgeInsets.zero, + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.timeline, + color: Colors.blue.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Riwayat Tindakan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ], ), const SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: tindakanList.length, - itemBuilder: (context, index) { - final tindakan = tindakanList[index]; - final bool isFirst = index == 0; - final bool isLast = index == tindakanList.length - 1; + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: tindakanList.length, + itemBuilder: (context, index) { + final tindakan = tindakanList[index]; + final bool isFirst = index == 0; + final bool isLast = index == tindakanList.length - 1; - return _buildTimelineTile( - context, - tindakan, - isFirst, - isLast, - ); - }, + return _buildTimelineTile( + context, + tindakan, + isFirst, + isLast, + ); + }, + ), ), ], ), @@ -1119,10 +868,10 @@ class DetailPengaduanView extends GetView { Color dotColor; switch (tindakan.statusTindakan) { case 'SELESAI': - dotColor = statusSelesaiColor; + dotColor = Colors.green; break; case 'PROSES': - dotColor = statusTindakanColor; + dotColor = Colors.blue; break; default: dotColor = Colors.grey; @@ -1133,48 +882,70 @@ class DetailPengaduanView extends GetView { isFirst: isFirst, isLast: isLast, indicatorStyle: IndicatorStyle( - width: 20, + width: 24, + height: 24, color: dotColor, + padding: const EdgeInsets.symmetric(vertical: 2), iconStyle: IconStyle( color: Colors.white, iconData: tindakan.statusTindakan == 'SELESAI' ? Icons.check : Icons.sync, + fontSize: 14, ), ), + beforeLineStyle: LineStyle( + color: Colors.grey.shade300, + thickness: 2, + ), + afterLineStyle: LineStyle( + color: Colors.grey.shade300, + thickness: 2, + ), endChild: Container( - margin: const EdgeInsets.only(left: 16, bottom: 24), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset(0, 1), + margin: const EdgeInsets.only(left: 20, bottom: 30), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: Colors.grey.shade200, ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan kategori dan status - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - tindakan.kategoriTindakanText, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan kategori dan status + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: dotColor.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - Row( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Menggunakan StatusPill untuk status tindakan + Expanded( + child: Text( + tindakan.kategoriTindakanText, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: dotColor, + ), + ), + ), StatusPill( status: tindakan.statusTindakanText, backgroundColor: dotColor, @@ -1182,182 +953,305 @@ class DetailPengaduanView extends GetView { ), ], ), - ], - ), - const SizedBox(height: 8), - - // Deskripsi tindakan - Text( - tindakan.tindakan ?? '', - style: const TextStyle(fontSize: 14), - ), - - // Catatan tindakan (jika ada) - if (tindakan.catatan != null && tindakan.catatan!.isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - 'Catatan: ${tindakan.catatan}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - fontStyle: FontStyle.italic, - ), ), - ], - // Hasil tindakan (jika ada) - if (tindakan.hasilTindakan != null && - tindakan.hasilTindakan!.isNotEmpty) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.blue.shade100), - ), + // Content + Padding( + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Deskripsi tindakan + Text( + tindakan.tindakan ?? '', + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + ), + ), + + // Catatan tindakan (jika ada) + if (tindakan.catatan != null && + tindakan.catatan!.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.note, + size: 16, + color: Colors.grey.shade700, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Catatan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 4), + Text( + tindakan.catatan!, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ), + ], + + // Hasil tindakan (jika ada) + if (tindakan.hasilTindakan != null && + tindakan.hasilTindakan!.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.check_circle_outline, + size: 16, + color: Colors.blue.shade700, + ), + const SizedBox(width: 8), + Text( + 'Hasil Tindakan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + tindakan.hasilTindakan!, + style: TextStyle( + fontSize: 13, + color: Colors.blue.shade900, + height: 1.4, + ), + ), + ], + ), + ), + ], + + // Bukti tindakan (jika ada) + if (tindakan.buktiTindakan != null && + tindakan.buktiTindakan!.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'Bukti Tindakan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Container( + height: 100, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: tindakan.buktiTindakan!.map((bukti) { + return GestureDetector( + onTap: () => + showFullScreenImage(context, bukti), + child: Container( + width: 100, + height: 100, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.grey.shade300), + image: DecorationImage( + image: bukti.startsWith('http') + ? NetworkImage(bukti) + : FileImage(File(bukti)) + as ImageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + child: const Icon( + Icons.zoom_in, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ], + ), + ), + + // Footer dengan info petugas dan tanggal + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Column( children: [ Row( children: [ Icon( - Icons.check_circle_outline, - size: 16, - color: Colors.blue.shade700, + Icons.person, + size: 14, + color: Colors.grey.shade600, ), - const SizedBox(width: 4), - Text( - 'Hasil Tindakan:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.blue.shade700, + const SizedBox(width: 6), + Expanded( + child: Text( + 'Oleh: ${tindakan.namaPetugas}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + overflow: TextOverflow.ellipsis, + ), ), ), ], ), - const SizedBox(height: 4), - Text( - tindakan.hasilTindakan!, - style: TextStyle( - fontSize: 12, - color: Colors.blue.shade900, - ), + const SizedBox(height: 6), + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + tindakan.tanggalTindakan != null + ? DateFormat('dd MMM yyyy HH:mm', 'id_ID') + .format(tindakan.tanggalTindakan!) + : '-', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], ), ], ), ), - ], - // Bukti tindakan (jika ada) - if (tindakan.buktiTindakan != null && - tindakan.buktiTindakan!.isNotEmpty) ...[ - const SizedBox(height: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Bukti Tindakan:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.grey.shade800, + // Tampilkan tombol edit jika status PROSES + if (tindakan.statusTindakan == 'PROSES') ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), ), ), - const SizedBox(height: 8), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: tindakan.buktiTindakan!.map((bukti) { - return GestureDetector( - onTap: () => showFullScreenImage(context, bukti), - child: Container( - width: 80, - height: 80, - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(8), - image: DecorationImage( - image: bukti.startsWith('http') - ? NetworkImage(bukti) - : FileImage(File(bukti)) as ImageProvider, - fit: BoxFit.cover, - ), - ), - ), - ); - }).toList(), + child: TextButton.icon( + style: TextButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.blue), + ), + minimumSize: Size(double.infinity, 36), + ), + onPressed: () { + _showEditTindakanDialog(context, tindakan); + }, + icon: Icon( + Icons.update, + size: 18, + color: Colors.blue, + ), + label: Text( + 'Input Hasil Tindakan', + style: TextStyle( + color: Colors.blue, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), - ), - ], - ), - ], - - const SizedBox(height: 8), - - // Footer dengan info petugas dan tanggal - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Oleh: ${tindakan.namaPetugas}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - Text( - tindakan.tanggalTindakan != null - ? DateFormat('dd MMM yyyy HH:mm', 'id_ID') - .format(tindakan.tanggalTindakan!) - : '-', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, ), ), ], - ), - // Tampilkan tombol edit jika status PROSES - if (tindakan.statusTindakan == 'PROSES') ...[ - const SizedBox(height: 8), - //divider - Divider( - color: Colors.grey.shade400, - thickness: 1, - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: Colors.blue), - ), - minimumSize: Size(double.infinity, 36), - ), - onPressed: () { - _showEditTindakanDialog(context, tindakan); - }, - icon: Icon( - Icons.update, - size: 18, - color: Colors.blue, - ), - label: Text( - 'Input Hasil Tindakan', - style: TextStyle( - color: Colors.blue, - fontSize: 13, - ), - ), - ), ], - ], + ), ), ), ); @@ -2109,212 +2003,220 @@ class DetailPengaduanView extends GetView { ); } - void showFullScreenImage(BuildContext context, String imageUrl) { - // Buat controller untuk InteractiveViewer - final TransformationController transformationController = - TransformationController(); - - Get.dialog( - Dialog( - insetPadding: EdgeInsets.zero, - child: Stack( - fit: StackFit.expand, - children: [ - InteractiveViewer( - panEnabled: true, - minScale: 0.5, - maxScale: 4, - transformationController: transformationController, - child: imageUrl.startsWith('http') - ? Image.network( - imageUrl, - fit: BoxFit.contain, - 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.shade300, - child: const Center( - child: Icon( - Icons.error, - size: 50, - color: Colors.red, - ), - ), - ); - }, - ) - : Image.file( - File(imageUrl), - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey.shade300, - child: const Center( - child: Icon( - Icons.error, - size: 50, - color: Colors.red, - ), - ), - ); - }, - ), - ), - Positioned( - top: 20, - right: 20, - child: GestureDetector( - onTap: () => Get.back(), + void showFullScreenImage(BuildContext context, String imagePath) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + child: Stack( + alignment: Alignment.center, + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle, + width: double.infinity, + height: double.infinity, + color: Colors.black87, + ), + ), + InteractiveViewer( + panEnabled: true, + boundaryMargin: const EdgeInsets.all(20), + minScale: 0.5, + maxScale: 4.0, + child: CachedNetworkImage( + imageUrl: imagePath, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), ), - child: const Icon( - Icons.close, - color: Colors.white, + errorWidget: (context, url, error) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error, color: Colors.white, size: 32), + const SizedBox(height: 8), + Text( + 'Gagal memuat gambar', + style: TextStyle(color: Colors.white), + ), + ], ), ), ), - ), - Positioned( - bottom: 20, - right: 20, - child: Row( - children: [ - GestureDetector( - onTap: () { - // Zoom in - final Matrix4 matrix = - transformationController.value.clone(); - matrix.scale(1.5); - transformationController.value = matrix; - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.zoom_in, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - // Zoom out - final Matrix4 matrix = - transformationController.value.clone(); - matrix.scale(0.75); - transformationController.value = matrix; - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.zoom_out, - color: Colors.white, - ), - ), - ), - ], + Positioned( + top: 20, + right: 20, + child: IconButton( + icon: const Icon(Icons.close, color: Colors.white, size: 30), + onPressed: () => Navigator.pop(context), + ), ), - ), - ], - ), - ), + ], + ), + ); + }, ); } // Widget untuk menampilkan feedback dan rating warga Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.amber.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Feedback Warga', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + // Header section + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.feedback, + color: Colors.amber.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Feedback Warga', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.amber.shade700, + ), + ), + ], + ), + + // Divider + Padding( + padding: const EdgeInsets.only(top: 16), + child: Divider( + color: Colors.amber.shade200, + thickness: 1, ), ), - const Divider(height: 24), + + // Rating display if (pengaduan.ratingWarga != null) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - const Text( - 'Rating: ', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - Row( - children: List.generate(5, (index) { - return Icon( - index < (pengaduan.ratingWarga ?? 0) - ? Icons.star - : Icons.star_border, - color: Colors.amber, - size: 20, - ); - }), - ), - ], + Center( + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(5, (index) { + return Icon( + index < (pengaduan.ratingWarga ?? 0) + ? Icons.star + : Icons.star_border, + color: Colors.amber.shade700, + size: 24, + ); + }), + ), ), ), + const SizedBox(height: 16), + + // Feedback content or placeholder if (pengaduan.feedbackWarga != null && pengaduan.feedbackWarga!.isNotEmpty) Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.amber.shade50, - borderRadius: BorderRadius.circular(8), + color: Colors.white, + borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.amber.shade200), + boxShadow: [ + BoxShadow( + color: Colors.amber.shade100.withOpacity(0.5), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - child: Text( - pengaduan.feedbackWarga!, - style: TextStyle( - fontSize: 14, - color: Colors.amber.shade900, - fontStyle: FontStyle.italic, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.format_quote, + size: 18, + color: Colors.amber.shade400, + ), + const SizedBox(width: 8), + Text( + 'Komentar Warga:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.amber.shade800, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + pengaduan.feedbackWarga!, + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + fontStyle: FontStyle.italic, + ), + ), + ], ), ) else - Text( - 'Warga belum memberikan komentar', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - fontStyle: FontStyle.italic, + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 18, + color: Colors.grey.shade600, + ), + const SizedBox(width: 12), + Text( + 'Warga belum memberikan komentar', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], ), ), ], @@ -2322,4 +2224,422 @@ class DetailPengaduanView extends GetView { ), ); } + + Widget _buildTindakanStatusItem( + String status, + String label, + String description, + Color color, + IconData icon, + ) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + color: color, + size: 20, + ), + const SizedBox(width: 10), + Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: color, + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatusInfo( + status, + description, + color, + icon, + ), + ], + ), + ); + } + + Widget _buildHeaderWithStatus( + BuildContext context, + PengaduanModel pengaduan, + Color statusColor, + String statusText, + ) { + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + pengaduan.judul ?? 'Pengaduan', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 0.3, + ), + ), + ), + _getStatusPill(pengaduan.status), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Text( + pengaduan.deskripsi ?? '', + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.calendar_today, + size: 16, + color: Colors.blue.shade700, + ), + ), + const SizedBox(width: 12), + Text( + pengaduan.tanggalPengaduan != null + ? DateFormat('dd MMMM yyyy', 'id_ID') + .format(pengaduan.tanggalPengaduan!) + : '-', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + if (pengaduan.fotoPengaduan != null && + pengaduan.fotoPengaduan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + height: 120, + child: ListView( + scrollDirection: Axis.horizontal, + children: pengaduan.fotoPengaduan!.map((url) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => _showFullScreenImage(context, url), + child: Container( + width: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: url, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + const Icon(Icons.error), + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ), + const SizedBox(height: 20), + // Tombol untuk menambahkan tindakan (hanya jika status MENUNGGU atau TINDAKAN) + if (pengaduan.status == 'MENUNGGU' || + pengaduan.status == 'TINDAKAN') + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + _showTambahTindakanDialog(context, pengaduan.id!); + }, + icon: const Icon(Icons.add_task), + label: const Text('Tambah Tindakan'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + if (pengaduan.status == 'MENUNGGU') + Padding( + padding: const EdgeInsets.only(left: 8), + child: ElevatedButton.icon( + onPressed: () { + _updateStatusToTindakan(pengaduan.id!); + }, + icon: const Icon(Icons.play_arrow), + label: const Text('Proses'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + if (pengaduan.status == 'TINDAKAN') + Padding( + padding: const EdgeInsets.only(left: 8), + child: ElevatedButton.icon( + onPressed: () { + _handleSelesaikanPengaduan(pengaduan.id!); + }, + icon: const Icon(Icons.check_circle), + label: const Text('Selesai'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + // Helper method untuk mendapatkan StatusPill berdasarkan status + StatusPill _getStatusPill(String? status) { + switch (status?.toUpperCase()) { + case 'MENUNGGU': + return StatusPill( + status: 'Menunggu', + backgroundColor: Colors.orange, + textColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + ); + case 'TINDAKAN': + return StatusPill( + status: 'Tindakan', + backgroundColor: Colors.blue, + textColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + ); + case 'SELESAI': + return StatusPill.completed(status: 'Selesai'); + default: + return StatusPill( + status: status ?? 'Tidak Diketahui', + backgroundColor: Colors.grey, + textColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + ); + } + } + + // Metode untuk mengubah status pengaduan ke TINDAKAN + void _updateStatusToTindakan(String pengaduanId) async { + try { + await controller.updateStatusTindakan(pengaduanId); + Get.forceAppUpdate(); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal mengubah status: $e', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Metode untuk menyelesaikan pengaduan + void _handleSelesaikanPengaduan(String pengaduanId) async { + try { + // Periksa apakah semua tindakan sudah diselesaikan + final tindakanList = await controller.getTindakanPengaduan(pengaduanId); + bool allTindakanSelesai = true; + + if (tindakanList.isNotEmpty) { + allTindakanSelesai = tindakanList.every((t) { + return t.statusTindakan == 'SELESAI'; + }); + } + + if (!allTindakanSelesai) { + Get.snackbar( + 'Peringatan', + 'Semua tindakan harus diselesaikan terlebih dahulu sebelum menyelesaikan pengaduan', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + return; + } + + showDialog( + context: Get.context!, + builder: (context) => AlertDialog( + title: const Text('Konfirmasi'), + content: const Text( + 'Apakah Anda yakin ingin menyelesaikan pengaduan ini? Status pengaduan akan berubah menjadi SELESAI.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () async { + try { + await controller.selesaikanPengaduan(pengaduanId); + Navigator.pop(context); + Get.forceAppUpdate(); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal menyelesaikan pengaduan: $e', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: const Text('Ya, Selesaikan'), + ), + ], + ), + ); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal memeriksa status tindakan: $e', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + void _showFullScreenImage(BuildContext context, String imagePath) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + child: Stack( + children: [ + InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.black.withOpacity(0.7), + child: Center( + child: imagePath.startsWith('http') + ? CachedNetworkImage( + imageUrl: imagePath, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + ) + : Image.file(File(imagePath)), + ), + ), + ), + Positioned( + top: 20, + right: 20, + child: IconButton( + icon: const Icon( + Icons.close, + color: Colors.white, + size: 30, + ), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ), + ); + }, + ); + } } 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 1893944..52a7ce0 100644 --- a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart +++ b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart'; @@ -33,12 +33,46 @@ class DetailPenyaluranPage extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('Detail Penyaluran'), + title: const Text( + 'Detail Penyaluran', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), centerTitle: true, + backgroundColor: Colors.white, + elevation: 0.5, + shadowColor: Colors.grey.withOpacity(0.3), leading: IconButton( - icon: const Icon(Icons.arrow_back, color: AppTheme.primaryColor), + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, + color: AppTheme.primaryColor, size: 20), + ), onPressed: () => Get.back(), ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + onPressed: controller.refreshData, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.refresh, + color: AppTheme.primaryColor, size: 20), + ), + ), + ), + ], ), body: Obx(() { if (controller.isLoading.value) { @@ -84,16 +118,38 @@ class DetailPenyaluranPage extends StatelessWidget { if (status == 'AKTIF') { return FloatingActionButton( backgroundColor: AppTheme.primaryColor, + elevation: 4, onPressed: () => _showQrCodeScanner(context), tooltip: 'Scan QR Code', - child: const Icon(Icons.qr_code_scanner, color: Colors.white), + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.primaryColor, + AppTheme.secondaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon(Icons.qr_code_scanner, color: Colors.white), + ), ); } return showScrollToTop.value ? FloatingActionButton( mini: true, - backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.arrow_upward), + backgroundColor: Colors.white, + elevation: 4, onPressed: () { scrollController.animateTo( 0, @@ -101,6 +157,19 @@ class DetailPenyaluranPage extends StatelessWidget { curve: Curves.easeInOut, ); }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade100], + ), + ), + child: const Icon(Icons.arrow_upward, + color: AppTheme.primaryColor), + ), ) : const SizedBox.shrink(); }), @@ -140,81 +209,135 @@ class DetailPenyaluranPage extends StatelessWidget { final skema = controller.skemaBantuan.value; return Card( - elevation: 2, + elevation: 3, + shadowColor: Colors.grey.withOpacity(0.3), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan status - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan status + Container( + padding: const EdgeInsets.all(16.0), + decoration: const BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Informasi Penyaluran', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), + const Row( + children: [ + Icon( + Icons.info_outline_rounded, + color: Colors.white, + size: 24, + ), + SizedBox(width: 8), + Text( + 'Informasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], ), _buildStatusBadge(penyaluran.status ?? '-'), ], ), - const Divider(height: 24), + ), - // Informasi penyaluran - _buildInfoRow('Nama', penyaluran.nama ?? '-'), - _buildInfoRow( - 'Tanggal', - penyaluran.tanggalPenyaluran != null - ? DateTimeHelper.formatDateTime( - penyaluran.tanggalPenyaluran!) - : 'Belum dijadwalkan'), - // Tampilkan tanggal selesai jika status TERLAKSANA atau BATALTERLAKSANA - if (penyaluran.status == 'TERLAKSANA' || - penyaluran.status == 'BATALTERLAKSANA') - _buildInfoRow( - 'Tanggal Selesai', - penyaluran.tanggalSelesai != null - ? DateTimeHelper.formatDateTime( - penyaluran.tanggalSelesai!) - : '-'), - _buildInfoRow( - 'Jumlah Penerima', '${penyaluran.jumlahPenerima ?? 0} orang'), + // Informasi penyaluran + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nama dan tanggal dalam baris yang sama + _buildInfoItem(Icons.description_outlined, 'Nama Penyaluran', + penyaluran.nama ?? '-', AppTheme.secondaryColor), + const SizedBox(height: 16), + _buildInfoItem( + Icons.event, + 'Tanggal Penyaluran', + penyaluran.tanggalPenyaluran != null + ? DateTimeHelper.formatDateTime( + penyaluran.tanggalPenyaluran!) + : 'Belum dijadwalkan', + AppTheme.secondaryColor), + const SizedBox(height: 16), - // Informasi skema bantuan - if (skema != null) ...[ - const Divider(height: 24), - Row( - children: [ - const Icon(Icons.category, - size: 16, color: AppTheme.secondaryColor), - const SizedBox(width: 8), - Text( - 'Skema: ${skema.nama ?? '-'}', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: AppTheme.secondaryColor, - ), - ), + // Tampilkan tanggal selesai jika status TERLAKSANA atau BATALTERLAKSANA + if (penyaluran.status == 'TERLAKSANA' || + penyaluran.status == 'BATALTERLAKSANA') + _buildInfoItem( + Icons.event_available, + 'Tanggal Selesai', + penyaluran.tanggalSelesai != null + ? DateTimeHelper.formatDateTime( + penyaluran.tanggalSelesai!) + : '-', + AppTheme.secondaryColor), + + const SizedBox(height: 16), + _buildInfoItem( + Icons.people, + 'Jumlah Penerima', + '${penyaluran.jumlahPenerima ?? 0} orang', + AppTheme.secondaryColor), + + // Informasi skema bantuan + if (skema != null) ...[ + const Divider(height: 32, thickness: 1), + _buildInfoItem(Icons.category, 'Skema Bantuan', + skema.nama ?? '-', AppTheme.accentColor), ], - ), - const SizedBox(height: 8), + ], + ), + ), + ], + ), + ); + } + + // Widget untuk info item dengan icon + Widget _buildInfoItem( + IconData icon, String label, String value, Color iconColor) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: iconColor), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - skema.deskripsi ?? 'Tidak ada deskripsi', + label, style: TextStyle( - fontSize: 14, - color: Colors.grey[600], + fontSize: 13, + color: Colors.grey[700], + fontWeight: FontWeight.normal, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), ), ], - ], + ), ), - ), + ], ); } @@ -265,13 +388,23 @@ class DetailPenyaluranPage extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(20), ), - child: Text( - '${_getFilteredPenerima().length} Orang', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), + child: Row( + children: [ + const Icon( + Icons.groups, + size: 16, + color: AppTheme.primaryColor, + ), + const SizedBox(width: 4), + Text( + '${_getFilteredPenerima().length} Orang', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], ), )), ], @@ -283,7 +416,7 @@ class DetailPenyaluranPage extends StatelessWidget { // Search field dengan filter status Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + padding: const EdgeInsets.all(16), child: Column( children: [ // Search field dengan icon dan tombol hapus @@ -293,10 +426,10 @@ class DetailPenyaluranPage extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.1), + color: Colors.grey.withOpacity(0.15), spreadRadius: 1, - blurRadius: 4, - offset: const Offset(0, 1), + blurRadius: 5, + offset: const Offset(0, 2), ), ], ), @@ -304,6 +437,10 @@ class DetailPenyaluranPage extends StatelessWidget { controller: searchController, decoration: InputDecoration( hintText: 'Cari nama, NIK, atau alamat...', + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: 14, + ), prefixIcon: const Icon(Icons.search, color: AppTheme.primaryColor), suffixIcon: Obx(() => searchQuery.value.isNotEmpty @@ -331,7 +468,7 @@ class DetailPenyaluranPage extends StatelessWidget { filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 16), + vertical: 14, horizontal: 16), ), onChanged: (value) { searchQuery.value = value.toLowerCase(); @@ -342,16 +479,26 @@ class DetailPenyaluranPage extends StatelessWidget { const SizedBox(height: 16), // Filter status dengan label - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildFilterChip('Semua', true), - const SizedBox(width: 8), - _buildFilterChip('Sudah Menerima', false), - const SizedBox(width: 8), - _buildFilterChip('Belum Menerima', false), - ], + Container( + height: 50, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + const SizedBox(width: 4), + _buildFilterChip('Semua', true), + const SizedBox(width: 12), + _buildFilterChip('Sudah Menerima', false), + const SizedBox(width: 12), + _buildFilterChip('Belum Menerima', false), + ], + ), ), ), ], @@ -490,9 +637,20 @@ class DetailPenyaluranPage extends StatelessWidget { : AppTheme.errorColor; return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), decoration: BoxDecoration( - color: Colors.grey.shade50, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey.shade50, + Colors.white, + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), border: Border( bottom: BorderSide(color: Colors.grey.shade200, width: 1), ), @@ -500,47 +658,82 @@ class DetailPenyaluranPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Header dengan judul dan persentase Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Icon( - Icons.insert_chart_outlined, - size: 18, - color: Colors.grey.shade700, - ), - const SizedBox(width: 8), - Text( - 'Progres Penyaluran', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.grey.shade700, + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), ), + child: Icon( + Icons.insert_chart_outlined, + size: 20, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Progres Penyaluran', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 2), + Text( + 'Total $totalPenerima penerima', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + ], ), ], ), Container( padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: progressColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: progressColor.withOpacity(0.3)), ), - child: Text( - '${persentaseSudah.toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: progressColor, - ), + child: Row( + children: [ + Icon( + persentaseSudah > 75 + ? Icons.emoji_events + : persentaseSudah > 50 + ? Icons.trending_up + : Icons.trending_down, + size: 16, + color: progressColor, + ), + const SizedBox(width: 4), + Text( + '${persentaseSudah.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: progressColor, + ), + ), + ], ), ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 20), // Progress bar dengan label Column( @@ -552,40 +745,49 @@ class DetailPenyaluranPage extends StatelessWidget { Text( 'Sudah Menerima', style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - ), - ), - Text( - '$sudahMenerima dari $totalPenerima', - style: TextStyle( - fontSize: 12, + fontSize: 13, fontWeight: FontWeight.bold, color: Colors.grey.shade700, ), ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: progressColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '$sudahMenerima dari $totalPenerima', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: progressColor, + ), + ), + ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), Stack( children: [ // Background progress bar Container( - height: 12, + height: 16, width: double.infinity, decoration: BoxDecoration( color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(8), ), ), // Foreground progress bar FractionallySizedBox( widthFactor: persentaseSudah / 100, child: Container( - height: 12, + height: 16, decoration: BoxDecoration( color: progressColor, - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: progressColor.withOpacity(0.3), @@ -594,6 +796,18 @@ class DetailPenyaluranPage extends StatelessWidget { ), ], ), + child: Center( + child: persentaseSudah > 15 + ? Text( + '${persentaseSudah.toInt()}%', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ) + : null, + ), ), ), ], @@ -601,27 +815,29 @@ class DetailPenyaluranPage extends StatelessWidget { ], ), - const SizedBox(height: 16), + const SizedBox(height: 20), // Statistik detail Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( - child: _buildStatistikItem( - 'Sudah Menerima', - sudahMenerima, - AppTheme.successColor, - Icons.check_circle, - )), - const SizedBox(width: 6), + child: _buildStatistikItem( + 'Sudah Menerima', + sudahMenerima, + AppTheme.successColor, + Icons.check_circle, + ), + ), + const SizedBox(width: 12), Expanded( - child: _buildStatistikItem( - 'Belum Menerima', - belumMenerima, - AppTheme.warningColor, - Icons.pending, - )), + child: _buildStatistikItem( + 'Belum Menerima', + belumMenerima, + AppTheme.warningColor, + Icons.pending, + ), + ), ], ) ], @@ -632,41 +848,53 @@ class DetailPenyaluranPage extends StatelessWidget { Widget _buildStatistikItem( String label, int value, Color color, IconData icon) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 5, + offset: const Offset(0, 2), + ) + ], ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.2), - shape: BoxShape.circle, + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, ), - child: Icon(icon, color: color, size: 14), ), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + SizedBox( + height: 8, + ), + Row( children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 16), + ), + SizedBox( + width: 12, + ), Text( '$value', style: TextStyle( - fontSize: 18, + fontSize: 30, fontWeight: FontWeight.bold, color: color, ), ), - Text( - label, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - ), - ), ], ), ], @@ -697,70 +925,82 @@ class DetailPenyaluranPage extends StatelessWidget { // Cek apakah filter ini yang aktif isSelected = statusFilter.value == filterValue; - // Tentukan icon berdasarkan jenis filter + // Tentukan icon dan warna berdasarkan jenis filter IconData filterIcon; + Color chipColor; + if (label == 'Semua') { filterIcon = Icons.list_alt; + chipColor = AppTheme.primaryColor; } else if (label == 'Sudah Menerima') { filterIcon = Icons.check_circle; + chipColor = AppTheme.successColor; } else { filterIcon = Icons.pending; + chipColor = AppTheme.warningColor; } - return FilterChip( - avatar: Icon( - filterIcon, - size: 16, - color: isSelected ? Colors.white : AppTheme.primaryColor, - ), - label: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(label), - const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.3) - : AppTheme.primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), + return InkWell( + onTap: () { + statusFilter.value = filterValue; + }, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? chipColor : Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + if (isSelected) + BoxShadow( + color: chipColor.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + border: Border.all( + color: isSelected ? Colors.transparent : Colors.grey.shade300, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + filterIcon, + size: 16, + color: isSelected ? Colors.white : chipColor, ), - child: Text( - '$count', + const SizedBox(width: 6), + Text( + label, style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: isSelected ? Colors.white : AppTheme.primaryColor, + fontSize: 13, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Colors.white : Colors.black87, ), ), - ), - ], - ), - selected: isSelected, - // checkmarkColor: Colors.white, - showCheckmark: false, - selectedColor: AppTheme.primaryColor, - labelStyle: TextStyle( - color: isSelected ? Colors.white : Colors.black87, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - backgroundColor: Colors.white, - elevation: isSelected ? 0 : 1, - shadowColor: Colors.grey.withOpacity(0.2), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: isSelected ? Colors.transparent : Colors.grey.shade300, - width: 1, + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.3) + : chipColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.white : chipColor, + ), + ), + ), + ], ), ), - onSelected: (selected) { - if (selected) { - statusFilter.value = filterValue; - } - }, ); } @@ -781,47 +1021,66 @@ class DetailPenyaluranPage extends StatelessWidget { final warga = item.warga; final bool sudahMenerima = item.statusPenerimaan?.toUpperCase() == 'DITERIMA'; - final Color cardColor = Colors.white; - final Color borderColor = Colors.grey.shade300; + final Color statusColor = + sudahMenerima ? AppTheme.successColor : AppTheme.warningColor; return Card( - elevation: 2, + elevation: 3, + shadowColor: Colors.grey.withOpacity(0.2), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: borderColor, width: 1.5), + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: sudahMenerima + ? statusColor.withOpacity(0.3) + : Colors.grey.shade200, + width: 1.5, + ), ), - color: cardColor, child: InkWell( onTap: () => _showDetailPenerima(context, item), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Avatar - CircleAvatar( - radius: 24, - backgroundColor: sudahMenerima - ? AppTheme.successColor.withOpacity(0.2) - : AppTheme.primaryColor.withOpacity(0.1), - child: Text( - warga != null && warga['nama_lengkap'] != null - ? warga['nama_lengkap'] - .toString() - .substring(0, 1) - .toUpperCase() - : '?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: sudahMenerima - ? AppTheme.successColor - : AppTheme.primaryColor, - fontSize: 20, + // Avatar dengan border berwarna + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: statusColor.withOpacity(0.5), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: statusColor.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: CircleAvatar( + radius: 28, + backgroundColor: sudahMenerima + ? statusColor.withOpacity(0.15) + : Colors.grey.shade50, + child: Text( + warga != null && warga['nama_lengkap'] != null + ? warga['nama_lengkap'] + .toString() + .substring(0, 1) + .toUpperCase() + : '?', + style: TextStyle( + fontWeight: FontWeight.bold, + color: sudahMenerima ? statusColor : Colors.grey.shade700, + fontSize: 22, + ), ), ), ), - const SizedBox(width: 12), + const SizedBox(width: 16), // Informasi penerima Expanded( @@ -829,45 +1088,76 @@ class DetailPenyaluranPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // Nama dan NIK - Text( - warga != null - ? warga['nama_lengkap'] ?? 'Nama tidak tersedia' - : 'Nama tidak tersedia', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - 'NIK: ${warga != null ? warga['nik'] ?? '-' : '-'}', - style: TextStyle( - fontSize: 13, - color: Colors.grey[600], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + // Nama + Row( + children: [ + Expanded( + child: Text( + warga != null + ? warga['nama_lengkap'] ?? 'Nama tidak tersedia' + : 'Nama tidak tersedia', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + // Icon indicator (arrow or check) + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: sudahMenerima + ? statusColor.withOpacity(0.1) + : Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon( + sudahMenerima + ? Icons.check_circle + : Icons.arrow_forward, + size: 18, + color: sudahMenerima + ? statusColor + : Colors.grey.shade400, + ), + ), + ], ), const SizedBox(height: 4), + + // NIK dengan icon + Row( + children: [ + Icon( + Icons.credit_card, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'NIK: ${warga != null ? warga['nik'] ?? '-' : '-'}', + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Status penerimaan _buildStatusChipNew(item.statusPenerimaan ?? '-'), ], ), ), - - // Status dan icon - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.arrow_forward_ios, - size: 14, - color: - sudahMenerima ? AppTheme.successColor : Colors.grey), - ], - ), ], ), ), @@ -878,38 +1168,63 @@ class DetailPenyaluranPage extends StatelessWidget { Widget _buildStatusBadge(String status) { Color backgroundColor; Color textColor = Colors.white; + IconData statusIcon; String statusText = _getStatusText(status); switch (status.toUpperCase()) { case 'DIJADWALKAN': backgroundColor = AppTheme.processedColor; + statusIcon = Icons.calendar_today; break; case 'AKTIF': backgroundColor = AppTheme.scheduledColor; + statusIcon = Icons.play_circle_outline; break; case 'TERLAKSANA': backgroundColor = AppTheme.completedColor; + statusIcon = Icons.check_circle_outline; break; case 'BATALTERLAKSANA': backgroundColor = AppTheme.errorColor; + statusIcon = Icons.cancel_outlined; break; default: backgroundColor = AppTheme.infoColor; + statusIcon = Icons.info_outline; } return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(16), + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: backgroundColor.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], ), - child: Text( - statusText, - style: TextStyle( - color: textColor, - fontSize: 14, - fontWeight: FontWeight.bold, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + statusIcon, + size: 16, + color: backgroundColor, + ), + const SizedBox(width: 6), + Text( + statusText, + style: TextStyle( + color: backgroundColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], ), ); } @@ -920,8 +1235,32 @@ class DetailPenyaluranPage extends StatelessWidget { if (controller.isProcessing.value) { return Container( padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -3), + ), + ], + ), child: const Center( - child: CircularProgressIndicator(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 8), + Text( + 'Memproses...', + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), ), ); } @@ -951,34 +1290,65 @@ class DetailPenyaluranPage extends StatelessWidget { Widget cancelButton = Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.cancel), - label: const Text('Batalkan'), + label: const Text( + 'Batalkan', + style: TextStyle(fontWeight: FontWeight.bold), + ), style: OutlinedButton.styleFrom( foregroundColor: AppTheme.errorColor, - side: const BorderSide(color: AppTheme.errorColor), + side: const BorderSide(color: AppTheme.errorColor, width: 1.5), padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), onPressed: () => _showBatalkanDialog(context), ), ); if (status == 'AKTIF') { + final bool allReceived = controller.penerimaPenyaluran.every( + (penerima) => penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA'); + return buildButtonContainer([ Expanded( child: ElevatedButton.icon( - icon: const Icon(Icons.check_circle), - label: const Text('Selesaikan'), - style: ElevatedButton.styleFrom( - 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, + icon: allReceived + ? const Icon(Icons.check_circle) + : const Icon(Icons.info_outline), + label: Text( + 'Selesaikan', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: allReceived ? 16 : 14, + ), ), - onPressed: controller.penerimaPenyaluran.every((penerima) => - penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA') + style: ElevatedButton.styleFrom( + backgroundColor: + allReceived ? AppTheme.successColor : Colors.grey.shade300, + foregroundColor: + allReceived ? Colors.white : Colors.grey.shade700, + padding: const EdgeInsets.symmetric(vertical: 16), + elevation: allReceived ? 2 : 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: allReceived ? controller.selesaikanPenyaluran - : null, + : () => Get.snackbar( + 'Perhatian', + 'Masih ada penerima yang belum menerima bantuan', + backgroundColor: Colors.orange.shade100, + colorText: Colors.orange.shade800, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(16), + borderRadius: 10, + icon: const Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + ), + ), ), ), const SizedBox(width: 12), @@ -1047,53 +1417,161 @@ class DetailPenyaluranPage extends StatelessWidget { void _showBatalkanDialog(BuildContext context) { final TextEditingController alasanController = TextEditingController(); + final isAlasanEmpty = false.obs; - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Batalkan Penyaluran'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Masukkan alasan pembatalan penyaluran:'), - const SizedBox(height: 16), - TextField( - controller: alasanController, - decoration: const InputDecoration( - hintText: 'Alasan pembatalan', - border: OutlineInputBorder(), - ), - maxLines: 3, - ), - ], + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('Batal'), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), ), - ElevatedButton( - onPressed: () { - if (alasanController.text.trim().isEmpty) { - Get.snackbar( - 'Error', - 'Alasan pembatalan tidak boleh kosong', - backgroundColor: Colors.red, - colorText: Colors.white, - snackPosition: SnackPosition.BOTTOM, - ); - return; - } + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.errorColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppTheme.errorColor, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Batalkan Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.errorColor, + ), + ), + const SizedBox(height: 4), + Text( + 'Tindakan ini tidak dapat dibatalkan', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ), + ), - controller.batalkanPenyaluran(alasanController.text.trim()); - Get.back(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.errorColor, - ), - child: const Text('Batalkan'), + const SizedBox(height: 20), + + // Form alasan + const Text( + 'Masukkan alasan pembatalan:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Obx(() => TextField( + controller: alasanController, + decoration: InputDecoration( + hintText: 'Misalnya: Terjadi kesalahan data penerima...', + hintStyle: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + errorText: isAlasanEmpty.value + ? 'Alasan tidak boleh kosong' + : null, + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + const BorderSide(color: AppTheme.errorColor), + ), + contentPadding: const EdgeInsets.all(16), + ), + maxLines: 3, + onChanged: (value) { + if (isAlasanEmpty.value && value.trim().isNotEmpty) { + isAlasanEmpty.value = false; + } + }, + )), + + const SizedBox(height: 24), + + // Tombol aksi + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Get.back(), + 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, + ), + child: const Text( + 'Batal', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + if (alasanController.text.trim().isEmpty) { + isAlasanEmpty.value = true; + return; + } + + controller + .batalkanPenyaluran(alasanController.text.trim()); + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.errorColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Batalkan', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ], ), - ], + ), ), ); } @@ -1783,31 +2261,44 @@ class DetailPenyaluranPage extends StatelessWidget { // Widget untuk status chip baru Widget _buildStatusChipNew(String status) { - final Color statusColor; - final String statusText; - - if (status.toUpperCase() == 'DITERIMA') { - statusColor = AppTheme.successColor; - statusText = 'Sudah Menerima'; - } else { - statusColor = AppTheme.warningColor; - statusText = 'Belum Menerima'; - } + 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: 12, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: statusColor.withOpacity(0.2), + 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: Text( - statusText, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: statusColor, - ), + 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, + ), + ), + ], ), ); } diff --git a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart index 88eed0f..827f29e 100644 --- a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart +++ b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart @@ -8,7 +8,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:signature/signature.dart'; import 'dart:io'; import 'dart:typed_data'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class KonfirmasiPenerimaPage extends StatefulWidget { final PenerimaPenyaluranModel penerima; diff --git a/lib/app/modules/petugas_desa/views/pengaduan_view.dart b/lib/app/modules/petugas_desa/views/pengaduan_view.dart index 8c626a5..acb7ceb 100644 --- a/lib/app/modules/petugas_desa/views/pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/pengaduan_view.dart @@ -2,7 +2,7 @@ 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'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class PengaduanView extends GetView { const PengaduanView({super.key}); diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index 33e9f63..197aff0 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart'; import 'dart:io'; diff --git a/lib/app/modules/petugas_desa/views/penyaluran_view.dart b/lib/app/modules/petugas_desa/views/penyaluran_view.dart index bd559b7..340ed42 100644 --- a/lib/app/modules/petugas_desa/views/penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/penyaluran_view.dart @@ -8,7 +8,6 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_ class PenyaluranView extends GetView { const PenyaluranView({super.key}); - @override Widget build(BuildContext context) { return DefaultTabController( @@ -24,6 +23,10 @@ class PenyaluranView extends GetView { labelColor: AppTheme.primaryColor, indicatorColor: AppTheme.primaryColor, unselectedLabelColor: Colors.grey, + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 13, + ), ), Expanded( child: TabBarView( @@ -38,10 +41,13 @@ class PenyaluranView extends GetView { ), ], ), - floatingActionButton: FloatingActionButton( + floatingActionButton: FloatingActionButton.extended( onPressed: () => Get.to(() => const TambahPenyaluranView()), backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.add, color: Colors.white), + icon: const Icon(Icons.add, color: Colors.white), + label: const Text('Tambah Jadwal', + style: TextStyle(color: Colors.white)), + elevation: 2, ), ), ); @@ -53,12 +59,12 @@ class PenyaluranView extends GetView { child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), child: Obx(() { if (controller.isLoading.value) { - return const Center( - child: Padding( - padding: EdgeInsets.all(32.0), + return const SizedBox( + height: 300, + child: Center( child: CircularProgressIndicator(), ), ); @@ -70,7 +76,7 @@ class PenyaluranView extends GetView { // Ringkasan jadwal _buildJadwalSummary(Get.context!), - const SizedBox(height: 20), + const SizedBox(height: 24), // Jadwal hari ini JadwalSectionWidget( @@ -80,7 +86,7 @@ class PenyaluranView extends GetView { status: 'Aktif', ), - const SizedBox(height: 20), + const SizedBox(height: 24), // Jadwal mendatang JadwalSectionWidget( @@ -90,7 +96,7 @@ class PenyaluranView extends GetView { status: 'Terjadwal', ), - const SizedBox(height: 50), + const SizedBox(height: 60), ], ); }), @@ -105,12 +111,12 @@ class PenyaluranView extends GetView { child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), child: Obx(() { if (controller.isLoading.value) { - return const Center( - child: Padding( - padding: EdgeInsets.all(32.0), + return const SizedBox( + height: 300, + child: Center( child: CircularProgressIndicator(), ), ); diff --git a/lib/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart b/lib/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart index d4a41df..4b47c56 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_pengaduan_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class RiwayatPengaduanView extends GetView { const RiwayatPengaduanView({super.key}); @@ -159,253 +159,388 @@ class RiwayatPengaduanView extends GetView { formattedDate = DateTimeHelper.formatDate(item.createdAt); } - return InkWell( - onTap: () { - // Navigasi ke halaman detail pengaduan - Get.toNamed('/detail-pengaduan', arguments: {'id': item.id}); - }, - borderRadius: BorderRadius.circular(12), - child: 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), - ), - ], + Color statusColor = AppTheme.successColor; + IconData statusIcon = Icons.check_circle; + + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: statusColor.withOpacity(0.3), + width: 1, ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + ), + elevation: 3, + child: InkWell( + onTap: () { + // Navigasi ke halaman detail pengaduan + Get.toNamed('/detail-pengaduan', arguments: {'id': item.id}); + }, + borderRadius: BorderRadius.circular(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header dengan warna sesuai status + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Text( - item.warga?['nama'] ?? item.judul ?? '', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + child: Row( + children: [ + Icon( + Icons.report_problem, + color: statusColor, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + item.warga?['nama'] ?? item.judul ?? '', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: statusColor, + ), + overflow: TextOverflow.ellipsis, ), + ), + ], ), ), - const SizedBox(width: 12), Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( - color: AppTheme.successColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: statusColor, + width: 1.0, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.check_circle, - size: 16, - color: AppTheme.successColor, + Icon( + statusIcon, + size: 14, + color: statusColor, ), const SizedBox(width: 4), Text( 'SELESAI', - style: - Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppTheme.successColor, - fontWeight: FontWeight.bold, - ), + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ], ), ), ], ), - const SizedBox(height: 8), - Text( - item.deskripsi ?? '', - style: Theme.of(context).textTheme.bodyMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 12), - Row( + ), + + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: _buildItemDetail( - context, - icon: Icons.person, - label: 'Pelapor', - value: item.warga?['nama_lengkap'] ?? '', - ), - ), - Expanded( - child: _buildItemDetail( - context, - icon: Icons.numbers, - label: 'NIK', - value: item.warga?['nik'] ?? '', - ), - ), - ], - ), - const SizedBox(height: 12), - if (item.penerimaPenyaluran != null) ...[ - Row( - children: [ - Expanded( - child: _buildItemDetail( - context, - icon: Icons.shopping_bag, - label: 'Jumlah', - value: - '${item.jumlahBantuan} ${item.stokBantuan['satuan']}', - )), - Expanded( - child: _buildItemDetail( - context, - icon: Icons.inventory, - label: 'Stok Bantuan', - value: item.stokBantuan['nama'] ?? '', + // Deskripsi masalah + if (item.deskripsi != null && item.deskripsi.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey.shade200, + width: 1.0, + ), ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: _buildItemDetail( - context, - icon: Icons.category, - label: 'Nama Penyaluran', - value: item.namaPenyaluran ?? '', - ), - ), - Expanded( - child: _buildItemDetail( - context, - icon: Icons.calendar_today, - label: 'Tanggal', - value: formattedDate, - ), - ), - ], - ), - ], - if (item.ratingWarga != null && item.ratingWarga > 0) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.amber.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.amber.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Feedback Warga', - style: const TextStyle( + 'Deskripsi Masalah:', + style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.amber, + color: Colors.grey.shade800, ), ), - Row( - children: List.generate(5, (index) { - return Icon( - index < (item.ratingWarga ?? 0) - ? Icons.star - : Icons.star_border, - color: Colors.amber, - size: 16, - ); - }), + const SizedBox(height: 6), + Text( + item.deskripsi ?? '', + style: TextStyle( + color: Colors.grey.shade700, + ), ), ], ), - if (item.feedbackWarga != null && - item.feedbackWarga.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - '${item.feedbackWarga}', - style: Theme.of(context).textTheme.bodySmall, + ), + + // Informasi penyaluran bantuan jika ada + if (item.penerimaPenyaluran != null) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.blue.shade200, + width: 1.0, ), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Penyaluran: ${item.namaPenyaluran ?? "Tidak tersedia"}', + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Bantuan', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + item.stokBantuan['nama'] ?? '-', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jumlah', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + '${item.jumlahBantuan} ${item.stokBantuan['satuan'] ?? ''}', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + + // Informasi pelapor dan NIK + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pelapor', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + item.warga?['nama_lengkap'] ?? '-', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'NIK', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + item.warga?['nik'] ?? '-', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), ], ), - ), - ], - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - 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), + + const SizedBox(height: 12), + + // Informasi tanggal + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.calendar_today, + size: 12, + color: Colors.grey.shade700, + ), + const SizedBox(width: 4), + Text( + formattedDate, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + + // Rating dan feedback warga jika ada + if (item.ratingWarga != null && item.ratingWarga > 0) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.amber.shade200, + width: 1.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Feedback Warga', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.amber.shade800, + ), + ), + Row( + children: List.generate(5, (index) { + return Icon( + index < (item.ratingWarga ?? 0) + ? Icons.star + : Icons.star_border, + color: Colors.amber, + size: 16, + ); + }), + ), + ], + ), + if (item.feedbackWarga != null && + item.feedbackWarga.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + '${item.feedbackWarga}', + style: TextStyle( + color: Colors.grey.shade700, + ), + ), + ], + ], + ), + ), + ], + + const SizedBox(height: 12), + + // Tombol detail + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: () { + Get.toNamed('/detail-pengaduan', + arguments: {'id': item.id}); + }, + icon: const Icon(Icons.info_outline, size: 18), + label: const Text('Lihat Detail'), + style: ElevatedButton.styleFrom( + backgroundColor: statusColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], ), ], ), - ], - ), + ), + ], ), ), ); } - - 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, - ), - ], - ), - ), - ], - ); - } } diff --git a/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart b/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart index f6ecc18..44f1eea 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_penitipan_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class RiwayatPenitipanView extends GetView { @@ -208,151 +208,348 @@ class RiwayatPenitipanView extends GetView { return Container( width: double.infinity, - margin: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.grey.withAlpha(26), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 1), + color: Colors.grey.withOpacity(0.15), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 3), ), ], + border: Border.all( + color: statusColor.withOpacity(0.3), + width: 1, + ), ), - child: Padding( - padding: const EdgeInsets.all(16.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - donaturNama, - 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, + // Header dengan status + Container( + color: statusColor.withOpacity(0.1), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( children: [ - Icon( - statusIcon, - size: 16, - color: statusColor, + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + statusIcon, + size: 16, + color: statusColor, + ), ), - const SizedBox(width: 4), + const SizedBox(width: 8), Text( item.status ?? 'Tidak diketahui', - style: Theme.of(context).textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: statusColor, fontWeight: FontWeight.bold, ), ), ], ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildItemDetail( - context, - icon: isUang ? Icons.monetization_on : Icons.category, - label: 'Kategori Bantuan', - value: kategoriNama, - ), - ), - Expanded( - child: _buildItemDetail( - context, - icon: - isUang ? Icons.account_balance_wallet : Icons.inventory, - label: 'Jumlah', - value: isUang - ? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}' - : '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan', - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: _buildItemDetail( - context, - icon: Icons.calendar_today, - label: item.status == 'TERVERIFIKASI' - ? 'Tanggal Verifikasi' - : 'Tanggal Penolakan', - value: DateTimeHelper.formatDateTime( - item.status == 'TERVERIFIKASI' - ? item.tanggalVerifikasi - : item.updatedAt, - defaultValue: 'Tidak ada tanggal'), - ), - ), - if (item.status == 'TERVERIFIKASI' && - item.petugasDesaId != null) - Expanded( - child: GetBuilder( - id: 'petugas_data', - builder: (controller) => _buildItemDetail( - context, - icon: Icons.person, - label: 'Diverifikasi Oleh', - value: - controller.getPetugasDesaNama(item.petugasDesaId), - ), - ), - ), - ], - ), - if (item.alasanPenolakan != null && - item.alasanPenolakan!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _buildItemDetail( - context, - icon: Icons.info_outline, - label: 'Alasan Penolakan', - value: item.alasanPenolakan!, + Text( + DateTimeHelper.formatDate(item.createdAt), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, + ), ), ], ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: () { - _showDetailDialog(context, item, donaturNama); - }, - icon: const Icon(Icons.info_outline, size: 18), - label: const Text('Detail'), - style: TextButton.styleFrom( - foregroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 8), + ), + + // Content + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Donatur info + Row( + children: [ + CircleAvatar( + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + radius: 20, + child: Text( + donaturNama.isNotEmpty + ? donaturNama.substring(0, 1).toUpperCase() + : '?', + style: TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + donaturNama, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + 'Donatur', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + ), + ], ), + + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 12), + + // Informasi bantuan + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isUang + ? Colors.green.withOpacity(0.1) + : Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isUang + ? Icons.monetization_on + : Icons.category, + size: 16, + color: isUang ? Colors.green : Colors.blue, + ), + const SizedBox(width: 6), + Text( + 'Kategori', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: isUang + ? Colors.green + : Colors.blue, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + kategoriNama, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isUang + ? Colors.amber.withOpacity(0.1) + : Colors.purple.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isUang + ? Icons.account_balance_wallet + : Icons.inventory, + size: 16, + color: isUang + ? Colors.amber.shade800 + : Colors.purple, + ), + const SizedBox(width: 6), + Text( + 'Jumlah', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: isUang + ? Colors.amber.shade800 + : Colors.purple, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + isUang + ? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}' + : '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan', + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ], + ), + + // Verifikasi info + if (item.status == 'TERVERIFIKASI' && + item.petugasDesaId != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.verified_user, + size: 16, + color: Colors.green, + ), + const SizedBox(width: 6), + Expanded( + child: RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + children: [ + TextSpan( + text: 'Diverifikasi oleh ', + style: TextStyle( + color: Colors.grey.shade700), + ), + TextSpan( + text: controller.getPetugasDesaNama( + item.petugasDesaId), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + + // Alasan penolakan jika ditolak + if (item.status == 'DITOLAK' && + item.alasanPenolakan != null && + item.alasanPenolakan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: Colors.red, + ), + const SizedBox(width: 6), + Text( + 'Alasan Penolakan', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + item.alasanPenolakan!, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + ), + ), + + // Footer dengan tombol aksi + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), ), - ], + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: () { + _showDetailDialog(context, item, donaturNama); + }, + icon: const Icon(Icons.info_outline, size: 16), + label: const Text('Lihat Detail'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + ), + ), + ], + ), ), ], ), @@ -360,42 +557,6 @@ class RiwayatPenitipanView extends GetView { ); } - 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, - ), - ], - ), - ), - ], - ); - } - void _showDetailDialog( BuildContext context, PenitipanBantuanModel item, String donaturNama) { final kategoriNama = item.kategoriBantuan?.nama ?? diff --git a/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart index f54f486..4990e42 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class RiwayatPenyaluranView extends GetView { diff --git a/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart b/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart index 1a524a3..fccd7a4 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_stok_view.dart @@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/data/models/riwayat_stok_model.dart'; import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:cached_network_image/cached_network_image.dart'; class RiwayatStokView extends GetView { @@ -60,60 +60,6 @@ class RiwayatStokView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header dengan latar belakang gradient - Container( - padding: const EdgeInsets.all(20.0), - decoration: BoxDecoration( - gradient: AppTheme.primaryGradient, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - spreadRadius: 1, - offset: const Offset(0, 3), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Heading - Row( - children: [ - const Icon( - Icons.inventory_2_outlined, - color: Colors.white, - size: 30, - ), - const SizedBox(width: 10), - Text( - 'Riwayat Stok Bantuan', - style: - Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ], - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.only(left: 40), - child: Text( - 'Catatan perubahan stok bantuan di desa Anda', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white.withOpacity(0.9), - ), - ), - ), - ], - ), - ), - // Filter dan pencarian Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 64fdb3f..1aeed6d 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class StokBantuanView extends GetView { const StokBantuanView({super.key}); @@ -17,13 +17,15 @@ class StokBantuanView extends GetView { ? const Center(child: CircularProgressIndicator()) : _buildContent(context)), ), - floatingActionButton: FloatingActionButton( + floatingActionButton: FloatingActionButton.extended( onPressed: () { // Tampilkan dialog tambah stok bantuan _showAddStokDialog(context); }, backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.add, color: Colors.white), + icon: const Icon(Icons.add, color: Colors.white), + label: const Text('Tambah Stok', style: TextStyle(color: Colors.white)), + elevation: 2, ), ); } 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 701ee0d..8412768 100644 --- a/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/tambah_penyaluran_view.dart @@ -3,7 +3,7 @@ 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/data/models/skema_bantuan_model.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; class TambahPenyaluranView extends GetView { const TambahPenyaluranView({super.key}); diff --git a/lib/app/modules/warga/views/detail_pengaduan_view.dart b/lib/app/modules/warga/views/detail_pengaduan_view.dart index c5a4a62..d95d94e 100644 --- a/lib/app/modules/warga/views/detail_pengaduan_view.dart +++ b/lib/app/modules/warga/views/detail_pengaduan_view.dart @@ -36,34 +36,100 @@ class WargaDetailPengaduanView extends GetView { title: const Text('Detail Pengaduan'), elevation: 0, ), - body: FutureBuilder>( - future: controller.getDetailPengaduan(pengaduanId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Text('Error: ${snapshot.error}'), - ); - } - - final data = snapshot.data; - if (data == null || data['pengaduan'] == null) { - return const Center( - child: Text('Data pengaduan tidak ditemukan'), - ); - } - - final pengaduan = PengaduanModel.fromJson(data['pengaduan']); - final List tindakanList = - (data['tindakan'] as List) - .map((item) => TindakanPengaduanModel.fromJson(item)) - .toList(); - - return _buildDetailContent(context, pengaduan, tindakanList); + body: RefreshIndicator( + onRefresh: () async { + await controller.getDetailPengaduan(pengaduanId); }, + color: AppTheme.primaryColor, + child: FutureBuilder>( + future: controller.getDetailPengaduan(pengaduanId), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(AppTheme.primaryColor), + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 60, + color: Colors.red.shade300, + ), + const SizedBox(height: 16), + Text( + 'Error: ${snapshot.error}', + style: const TextStyle( + fontSize: 16, + color: Colors.red, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + controller.getDetailPengaduan(pengaduanId); + }, + icon: const Icon(Icons.refresh), + label: const Text('Coba Lagi'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + ), + ), + ], + ), + ); + } + + final data = snapshot.data; + if (data == null || data['pengaduan'] == null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.search_off, + size: 60, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Data pengaduan tidak ditemukan', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Text( + 'Pengaduan mungkin telah dihapus atau tidak tersedia', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ); + } + + final pengaduan = PengaduanModel.fromJson(data['pengaduan']); + final List tindakanList = + (data['tindakan'] as List) + .map((item) => TindakanPengaduanModel.fromJson(item)) + .toList(); + + return _buildDetailContent(context, pengaduan, tindakanList); + }, + ), ), floatingActionButton: FutureBuilder>( future: controller.getDetailPengaduan(pengaduanId), @@ -89,7 +155,12 @@ class WargaDetailPengaduanView extends GetView { backgroundColor: AppTheme.primaryColor, icon: const Icon(Icons.star, color: Colors.white), label: const Text('Beri Rating', - style: TextStyle(color: Colors.white)), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), ); } @@ -126,33 +197,49 @@ class WargaDetailPengaduanView extends GetView { statusText = pengaduan.status ?? 'Tidak Diketahui'; } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan status - _buildHeaderWithStatus(context, pengaduan, statusColor, statusText), + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan status + _buildHeaderWithStatus(context, pengaduan, statusColor, statusText), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Informasi penyaluran yang diadukan - if (pengaduan.penerimaPenyaluran != null) - _buildPenyaluranInfo(context, pengaduan), + // Informasi penyaluran yang diadukan + if (pengaduan.penerimaPenyaluran != null) + _buildPenyaluranInfo(context, pengaduan), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Tampilkan feedback dan rating jika sudah ada - if (pengaduan.status?.toUpperCase() == 'SELESAI' && - (pengaduan.feedbackWarga != null || - pengaduan.ratingWarga != null)) - _buildFeedbackSection(context, pengaduan), + // Tampilkan feedback dan rating jika sudah ada + if (pengaduan.status?.toUpperCase() == 'SELESAI' && + (pengaduan.feedbackWarga != null || + pengaduan.ratingWarga != null)) + _buildFeedbackSection(context, pengaduan), - const SizedBox(height: 24), + if (pengaduan.status?.toUpperCase() == 'SELESAI' && + (pengaduan.feedbackWarga != null || + pengaduan.ratingWarga != null)) + const SizedBox(height: 24), - // Timeline tindakan - _buildTindakanTimeline(context, tindakanList), - ], + // Timeline tindakan + _buildTindakanTimeline(context, tindakanList), + + // Padding di bagian bawah untuk memberikan space saat ada floating action button + const SizedBox(height: 80), + ], + ), ), ); } @@ -160,66 +247,161 @@ class WargaDetailPengaduanView extends GetView { // Widget untuk menampilkan feedback dan rating Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.amber.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Header section Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.feedback, + color: Colors.amber.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( 'Feedback Anda', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, + color: Colors.amber.shade700, ), ), - if (pengaduan.ratingWarga != null) - Row( + ], + ), + + // Divider + Padding( + padding: const EdgeInsets.only(top: 16), + child: Divider( + color: Colors.amber.shade200, + thickness: 1, + ), + ), + + // Rating display + if (pengaduan.ratingWarga != null) + Center( + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: List.generate(5, (index) { return Icon( index < (pengaduan.ratingWarga ?? 0) ? Icons.star : Icons.star_border, - color: Colors.amber, - size: 20, + color: Colors.amber.shade700, + size: 24, ); }), ), - ], - ), - const Divider(height: 24), + ), + ), + const SizedBox(height: 8), + + // Feedback content or placeholder if (pengaduan.feedbackWarga != null && pengaduan.feedbackWarga!.isNotEmpty) Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.amber.shade50, - borderRadius: BorderRadius.circular(8), + color: Colors.white, + borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.amber.shade200), + boxShadow: [ + BoxShadow( + color: Colors.amber.shade100.withOpacity(0.5), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - child: Text( - pengaduan.feedbackWarga!, - style: TextStyle( - fontSize: 14, - color: Colors.amber.shade900, - fontStyle: FontStyle.italic, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.format_quote, + size: 18, + color: Colors.amber.shade400, + ), + const SizedBox(width: 8), + Text( + 'Komentar Anda:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.amber.shade800, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + pengaduan.feedbackWarga!, + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + fontStyle: FontStyle.italic, + ), + ), + ], ), ) else - Text( - 'Anda belum memberikan komentar', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - fontStyle: FontStyle.italic, + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 18, + color: Colors.grey.shade600, + ), + const SizedBox(width: 12), + Text( + 'Anda belum memberikan komentar', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], ), ), ], @@ -240,12 +422,35 @@ class WargaDetailPengaduanView extends GetView { builder: (context) => StatefulBuilder( builder: (context, setState) { return AlertDialog( - title: Text( - 'Beri Feedback Pelayanan', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Column( + children: [ + Icon( + Icons.rate_review, + color: AppTheme.primaryColor, + size: 40, + ), + const SizedBox(height: 10), + Text( + 'Beri Feedback Pelayanan', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Bantu kami meningkatkan layanan dengan memberi rating dan komentar', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], ), content: Form( key: formKey, @@ -261,9 +466,14 @@ class WargaDetailPengaduanView extends GetView { fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), - SingleChildScrollView( - scrollDirection: Axis.horizontal, + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 8), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(12), + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(5, (index) { @@ -278,7 +488,7 @@ class WargaDetailPengaduanView extends GetView { ? Icons.star : Icons.star_border, color: Colors.amber, - size: 30, + size: 32, ), padding: EdgeInsets.zero, constraints: BoxConstraints(), @@ -286,7 +496,7 @@ class WargaDetailPengaduanView extends GetView { }), ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), const Text( 'Komentar', style: TextStyle( @@ -294,14 +504,25 @@ class WargaDetailPengaduanView extends GetView { fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), TextFormField( controller: feedbackController, - maxLines: 3, + maxLines: 4, decoration: InputDecoration( hintText: 'Tulis komentar Anda di sini...', + filled: true, + fillColor: Colors.grey.shade50, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: AppTheme.primaryColor), ), ), validator: (value) { @@ -319,7 +540,12 @@ class WargaDetailPengaduanView extends GetView { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Batal'), + child: Text( + 'Batal', + style: TextStyle( + color: Colors.grey.shade600, + ), + ), ), ElevatedButton( onPressed: () async { @@ -342,12 +568,25 @@ class WargaDetailPengaduanView extends GetView { feedbackController.text, selectedRating, ); + + // Refresh data pengaduan + await controller.getDetailPengaduan(pengaduan.id!); } }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 2, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + child: Text( + 'Kirim Feedback', + style: TextStyle( + fontWeight: FontWeight.bold, + ), ), - child: Text('Kirim'), ), ], ); @@ -363,12 +602,20 @@ class WargaDetailPengaduanView extends GetView { String statusText, ) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -381,41 +628,60 @@ class WargaDetailPengaduanView extends GetView { style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + letterSpacing: 0.3, ), ), ), _getStatusPill(pengaduan.status), ], ), - const SizedBox(height: 12), - Text( - pengaduan.deskripsi ?? '', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Text( + pengaduan.deskripsi ?? '', + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + ), ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), Row( children: [ - Icon( - Icons.calendar_today, - size: 16, - color: Colors.grey.shade600, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.calendar_today, + size: 16, + color: Colors.blue.shade700, + ), ), - const SizedBox(width: 8), + const SizedBox(width: 12), Text( pengaduan.tanggalPengaduan != null ? DateFormat('dd MMMM yyyy', 'id_ID') .format(pengaduan.tanggalPengaduan!) : '-', style: TextStyle( - color: Colors.grey.shade600, + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, ), ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 20), // Panel status pengaduan _buildStatusPanel(context, pengaduan), ], @@ -458,20 +724,32 @@ class WargaDetailPengaduanView extends GetView { return Container( width: double.infinity, - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), + color: _getStatusColor(status).withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _getStatusColor(status).withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Status Pengaduan', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + Row( + children: [ + Icon( + Icons.info_outline, + color: _getStatusColor(status), + size: 20, + ), + const SizedBox(width: 10), + Text( + 'Status Pengaduan', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: _getStatusColor(status), + ), + ), + ], ), const SizedBox(height: 12), _buildStatusGuideItem( @@ -492,10 +770,11 @@ class WargaDetailPengaduanView extends GetView { IconData icon, ) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 32, - height: 32, + width: 36, + height: 36, decoration: BoxDecoration( color: color.withOpacity(0.2), shape: BoxShape.circle, @@ -504,11 +783,11 @@ class WargaDetailPengaduanView extends GetView { child: Icon( icon, color: color, - size: 18, + size: 20, ), ), ), - const SizedBox(width: 12), + const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -517,13 +796,16 @@ class WargaDetailPengaduanView extends GetView { status: _getStatusText(status), backgroundColor: color, textColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), - const SizedBox(height: 4), + const SizedBox(height: 8), Text( description, style: TextStyle( - fontSize: 12, + fontSize: 14, color: Colors.grey.shade700, + height: 1.4, ), ), ], @@ -587,67 +869,113 @@ class WargaDetailPengaduanView extends GetView { Widget _buildPenyaluranInfo(BuildContext context, PengaduanModel pengaduan) { return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.blue.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Informasi Penyaluran', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - Icon( - Icons.inventory, - color: AppTheme.primaryColor, + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.inventory_2_rounded, + color: AppTheme.primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + 'Informasi Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], ), ], ), - const Divider(height: 24), - _buildInfoRow('Nama Penyaluran', pengaduan.namaPenyaluran), - _buildInfoRow('Jenis Bantuan', pengaduan.jenisBantuan), - _buildInfoRow('Jumlah Bantuan', pengaduan.jumlahBantuan), - _buildInfoRow('Deskripsi', pengaduan.deskripsiPenyaluran), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Divider( + color: Colors.blue.shade100, + thickness: 1, + ), + ), + _buildInfoItem( + 'Nama Penyaluran', pengaduan.namaPenyaluran, Icons.assignment), + _buildInfoItem( + 'Jenis Bantuan', pengaduan.jenisBantuan, Icons.category), + _buildInfoItem('Jumlah Bantuan', pengaduan.jumlahBantuan, + Icons.shopping_basket), + _buildInfoItem( + 'Deskripsi', pengaduan.deskripsiPenyaluran, Icons.description), ], ), ), ); } - Widget _buildInfoRow(String label, String value) { + Widget _buildInfoItem(String label, String value, IconData icon) { return Padding( - padding: const EdgeInsets.only(bottom: 12.0), + padding: const EdgeInsets.only(bottom: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 120, - child: Text( - label, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 18, + color: AppTheme.primaryColor, ), ), - const SizedBox(width: 8), + const SizedBox(width: 16), Expanded( - child: Text( - value, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ], @@ -660,45 +988,87 @@ class WargaDetailPengaduanView extends GetView { List tindakanList, ) { if (tindakanList.isEmpty) { - return InfoCard( - title: 'Belum Ada Tindakan', - description: 'Pengaduan Anda sedang menunggu tindakan dari petugas', - icon: Icons.info_outline, - backgroundColor: Colors.grey.shade50, + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + padding: const EdgeInsets.all(20), + child: InfoCard( + title: 'Belum Ada Tindakan', + description: 'Pengaduan Anda sedang menunggu tindakan dari petugas', + icon: Icons.hourglass_empty, + backgroundColor: Colors.orange.shade50, + iconColor: Colors.orange, + ), + ), ); } return Card( - elevation: 2, + elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), ), - child: Padding( - padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], + ), + ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionHeader( - title: 'Riwayat Tindakan', - padding: EdgeInsets.zero, + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.timeline, + color: Colors.blue.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Riwayat Tindakan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ], ), const SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: tindakanList.length, - itemBuilder: (context, index) { - final tindakan = tindakanList[index]; - final bool isFirst = index == 0; - final bool isLast = index == tindakanList.length - 1; + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: tindakanList.length, + itemBuilder: (context, index) { + final tindakan = tindakanList[index]; + final bool isFirst = index == 0; + final bool isLast = index == tindakanList.length - 1; - return _buildTimelineTile( - context, - tindakan, - isFirst, - isLast, - ); - }, + return _buildTimelineTile( + context, + tindakan, + isFirst, + isLast, + ); + }, + ), ), ], ), @@ -729,48 +1099,70 @@ class WargaDetailPengaduanView extends GetView { isFirst: isFirst, isLast: isLast, indicatorStyle: IndicatorStyle( - width: 20, + width: 24, + height: 24, color: dotColor, + padding: const EdgeInsets.symmetric(vertical: 2), iconStyle: IconStyle( color: Colors.white, iconData: tindakan.statusTindakan == 'SELESAI' ? Icons.check : Icons.sync, + fontSize: 14, ), ), + beforeLineStyle: LineStyle( + color: Colors.grey.shade300, + thickness: 2, + ), + afterLineStyle: LineStyle( + color: Colors.grey.shade300, + thickness: 2, + ), endChild: Container( - margin: const EdgeInsets.only(left: 16, bottom: 24), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset(0, 1), + margin: const EdgeInsets.only(left: 20, bottom: 30), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: Colors.grey.shade200, ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan kategori dan status - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - tindakan.kategoriTindakanText, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan kategori dan status + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: dotColor.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - Row( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Menggunakan StatusPill untuk status tindakan + Expanded( + child: Text( + tindakan.kategoriTindakanText, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: dotColor, + ), + ), + ), StatusPill( status: tindakan.statusTindakanText, backgroundColor: dotColor, @@ -778,145 +1170,260 @@ class WargaDetailPengaduanView extends GetView { ), ], ), - ], - ), - const SizedBox(height: 8), - - // Deskripsi tindakan - Text( - tindakan.tindakan ?? '', - style: const TextStyle(fontSize: 14), - ), - - // Catatan tindakan (jika ada) - if (tindakan.catatan != null && tindakan.catatan!.isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - 'Catatan: ${tindakan.catatan}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - fontStyle: FontStyle.italic, - ), ), - ], - // Hasil tindakan (jika ada) - if (tindakan.hasilTindakan != null && - tindakan.hasilTindakan!.isNotEmpty) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.blue.shade100), - ), + // Content + Padding( + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Deskripsi tindakan + Text( + tindakan.tindakan ?? '', + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + height: 1.4, + ), + ), + + // Catatan tindakan (jika ada) + if (tindakan.catatan != null && + tindakan.catatan!.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.note, + size: 16, + color: Colors.grey.shade700, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Catatan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 4), + Text( + tindakan.catatan!, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ), + ], + + // Hasil tindakan (jika ada) + if (tindakan.hasilTindakan != null && + tindakan.hasilTindakan!.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.check_circle_outline, + size: 16, + color: Colors.blue.shade700, + ), + const SizedBox(width: 8), + Text( + 'Hasil Tindakan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + tindakan.hasilTindakan!, + style: TextStyle( + fontSize: 13, + color: Colors.blue.shade900, + height: 1.4, + ), + ), + ], + ), + ), + ], + + // Bukti tindakan (jika ada) + if (tindakan.buktiTindakan != null && + tindakan.buktiTindakan!.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'Bukti Tindakan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Container( + height: 100, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: tindakan.buktiTindakan!.map((bukti) { + return GestureDetector( + onTap: () => + showFullScreenImage(context, bukti), + child: Container( + width: 100, + height: 100, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.grey.shade300), + image: DecorationImage( + image: bukti.startsWith('http') + ? NetworkImage(bukti) + : FileImage(File(bukti)) + as ImageProvider, + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + child: const Icon( + Icons.zoom_in, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ], + ), + ), + + // Footer dengan info petugas dan tanggal + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Column( children: [ Row( children: [ Icon( - Icons.check_circle_outline, - size: 16, - color: Colors.blue.shade700, + Icons.person, + size: 14, + color: Colors.grey.shade600, ), - const SizedBox(width: 4), - Text( - 'Hasil Tindakan:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.blue.shade700, + const SizedBox(width: 6), + Expanded( + child: Text( + 'Oleh: ${tindakan.namaPetugas}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + overflow: TextOverflow.ellipsis, + ), ), ), ], ), - const SizedBox(height: 4), - Text( - tindakan.hasilTindakan!, - style: TextStyle( - fontSize: 12, - color: Colors.blue.shade900, - ), + const SizedBox(height: 6), + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + tindakan.tanggalTindakan != null + ? DateFormat('dd MMM yyyy HH:mm', 'id_ID') + .format(tindakan.tanggalTindakan!) + : '-', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], ), ], ), ), ], - - // Bukti tindakan (jika ada) - if (tindakan.buktiTindakan != null && - tindakan.buktiTindakan!.isNotEmpty) ...[ - const SizedBox(height: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Bukti Tindakan:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.grey.shade800, - ), - ), - const SizedBox(height: 8), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: tindakan.buktiTindakan!.map((bukti) { - return GestureDetector( - onTap: () => showFullScreenImage(context, bukti), - child: Container( - width: 80, - height: 80, - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(8), - image: DecorationImage( - image: bukti.startsWith('http') - ? NetworkImage(bukti) - : FileImage(File(bukti)) as ImageProvider, - fit: BoxFit.cover, - ), - ), - ), - ); - }).toList(), - ), - ), - ], - ), - ], - - const SizedBox(height: 8), - - // Footer dengan info petugas dan tanggal - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Oleh: ${tindakan.namaPetugas}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - Text( - tindakan.tanggalTindakan != null - ? DateFormat('dd MMM yyyy HH:mm', 'id_ID') - .format(tindakan.tanggalTindakan!) - : '-', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - ], - ), - ], + ), ), ), ); @@ -933,55 +1440,88 @@ class WargaDetailPengaduanView extends GetView { child: Stack( fit: StackFit.expand, children: [ - InteractiveViewer( - panEnabled: true, - minScale: 0.5, - maxScale: 4, - transformationController: transformationController, - child: imageUrl.startsWith('http') - ? Image.network( - imageUrl, - fit: BoxFit.contain, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, + Container( + color: Colors.black, + child: InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4, + transformationController: transformationController, + child: Center( + child: Hero( + tag: imageUrl, + child: imageUrl.startsWith('http') + ? Image.network( + imageUrl, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white), + value: loadingProgress.expectedTotalBytes != + null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.broken_image, + size: 60, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + 'Gagal memuat gambar', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + }, + ) + : Image.file( + File(imageUrl), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.broken_image, + size: 60, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + 'Gagal memuat gambar', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + }, ), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey.shade300, - child: const Center( - child: Icon( - Icons.error, - size: 50, - color: Colors.red, - ), - ), - ); - }, - ) - : Image.file( - File(imageUrl), - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey.shade300, - child: const Center( - child: Icon( - Icons.error, - size: 50, - color: Colors.red, - ), - ), - ); - }, - ), + ), + ), + ), ), Positioned( top: 20, @@ -989,45 +1529,28 @@ class WargaDetailPengaduanView extends GetView { child: GestureDetector( onTap: () => Get.back(), child: Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, + size: 24, ), ), ), ), Positioned( bottom: 20, - right: 20, + left: 0, + right: 0, child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - GestureDetector( - onTap: () { - // Zoom in - final Matrix4 matrix = - transformationController.value.clone(); - matrix.scale(1.5); - transformationController.value = matrix; - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.zoom_in, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - GestureDetector( + _buildImageControlButton( + icon: Icons.zoom_out, onTap: () { // Zoom out final Matrix4 matrix = @@ -1035,17 +1558,25 @@ class WargaDetailPengaduanView extends GetView { matrix.scale(0.75); transformationController.value = matrix; }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.zoom_out, - color: Colors.white, - ), - ), + ), + const SizedBox(width: 16), + _buildImageControlButton( + icon: Icons.refresh, + onTap: () { + // Reset + transformationController.value = Matrix4.identity(); + }, + ), + const SizedBox(width: 16), + _buildImageControlButton( + icon: Icons.zoom_in, + onTap: () { + // Zoom in + final Matrix4 matrix = + transformationController.value.clone(); + matrix.scale(1.5); + transformationController.value = matrix; + }, ), ], ), @@ -1055,6 +1586,27 @@ class WargaDetailPengaduanView extends GetView { ), ); } + + Widget _buildImageControlButton({ + required IconData icon, + required Function() onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 24, + ), + ), + ); + } } class TambahTindakanPengaduanView extends StatefulWidget { diff --git a/lib/app/modules/warga/views/warga_dashboard_view.dart b/lib/app/modules/warga/views/warga_dashboard_view.dart index 9489369..72b25d0 100644 --- a/lib/app/modules/warga/views/warga_dashboard_view.dart +++ b/lib/app/modules/warga/views/warga_dashboard_view.dart @@ -2,11 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; -import 'package:penyaluran_app/app/routes/app_pages.dart'; -import 'package:penyaluran_app/app/widgets/bantuan_card.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart'; -import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; -import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart'; class WargaDashboardView extends GetView { const WargaDashboardView({super.key}); @@ -32,8 +28,6 @@ class WargaDashboardView extends GetView { _buildStatisticSection(), const SizedBox(height: 24), _buildPenerimaanSummary(), - const SizedBox(height: 24), - _buildRecentPenerimaan(), ], ), ), @@ -588,285 +582,4 @@ class WargaDashboardView extends GetView { ], ); } - - Widget _buildRecentPenerimaan() { - if (controller.penerimaPenyaluran.isEmpty) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - SectionHeader( - title: 'Bantuan Terbaru', - titleStyle: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - padding: EdgeInsets.zero, - ), - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - ), - child: Column( - children: [ - Icon( - Icons.info_outline, - size: 48, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'Belum Ada Bantuan', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey.shade700, - ), - ), - const SizedBox(height: 8), - Text( - 'Data bantuan akan muncul di sini ketika Anda menerima bantuan.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - ), - ), - ], - ), - ), - ], - ), - ); - } - - final maxItems = controller.penerimaPenyaluran.length > 2 - ? 2 - : controller.penerimaPenyaluran.length; - - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - title: 'Bantuan Terbaru', - viewAllText: 'Lihat Semua', - onViewAll: () { - Get.toNamed(Routes.wargaPenerimaan); - }, - titleStyle: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - padding: EdgeInsets.zero, - ), - const SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: maxItems, - itemBuilder: (context, index) { - final item = controller.penerimaPenyaluran[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: BantuanCard( - item: item, - isCompact: true, - onTap: () { - Get.toNamed('/warga/detail-penerimaan', - arguments: {'id': item.id}); - }, - ), - ); - }, - ), - if (controller.penerimaPenyaluran.length > 2) - Center( - child: ElevatedButton.icon( - onPressed: () { - Get.toNamed(Routes.wargaPenerimaan); - }, - icon: const Icon(Icons.list), - label: const Text('Lihat Semua Bantuan'), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - ), - ), - ), - ], - ), - ); - } - - void _showBuatPengaduanDialog(BuildContext context) { - // Daftar penerimaan bantuan yang dapat diadukan (status DITERIMA) - final bantuanDiterima = controller.penerimaPenyaluran - .where((item) => item.statusPenerimaan == 'DITERIMA') - .toList(); - - // Jika tidak ada bantuan yang diterima - if (bantuanDiterima.isEmpty) { - Get.snackbar( - 'Informasi', - 'Tidak ada bantuan yang sudah diterima untuk dapat diajukan pengaduan', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, - colorText: Colors.white, - ); - return; - } - - // Variabel untuk menyimpan pilihan penerimaan - PenerimaPenyaluranModel? selectedPenerimaan = bantuanDiterima.first; - - Get.dialog( - Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - Icons.report_problem, - color: Colors.orange.shade700, - ), - const SizedBox(width: 10), - const Text( - 'Buat Pengaduan Baru', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Get.back(), - ), - ], - ), - const SizedBox(height: 20), - const Text( - 'Pilih Bantuan yang Ingin Diadukan:', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const SizedBox(height: 10), - StatefulBuilder( - builder: (context, setState) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: selectedPenerimaan, - items: bantuanDiterima.map((item) { - String displayText = item.namaPenyaluran ?? 'Bantuan'; - if (item.tanggalPenerimaan != null) { - displayText += - ' (${DateFormat('dd/MM/yyyy').format(item.tanggalPenerimaan!)})'; - } - - return DropdownMenuItem( - value: item, - child: Text(displayText), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - selectedPenerimaan = value; - }); - } - }, - ), - ), - ); - }, - ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - if (selectedPenerimaan != null) { - Get.back(); - Get.to( - () => FormPengaduanView( - uidPenerimaan: selectedPenerimaan!.id.toString(), - ), - transition: Transition.rightToLeft, - duration: const Duration(milliseconds: 300), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange.shade700, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Lanjutkan', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } } diff --git a/lib/app/modules/warga/views/warga_pengaduan_view.dart b/lib/app/modules/warga/views/warga_pengaduan_view.dart index 8038fc0..018e3a6 100644 --- a/lib/app/modules/warga/views/warga_pengaduan_view.dart +++ b/lib/app/modules/warga/views/warga_pengaduan_view.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.dart'; import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'dart:io'; import 'package:image_picker/image_picker.dart'; diff --git a/lib/app/modules/warga/views/warga_view.dart b/lib/app/modules/warga/views/warga_view.dart index 4b1a0e6..0221b98 100644 --- a/lib/app/modules/warga/views/warga_view.dart +++ b/lib/app/modules/warga/views/warga_view.dart @@ -14,19 +14,6 @@ class WargaView extends GetView { @override Widget build(BuildContext context) { - // Tambahkan listener untuk refresh data saat fokus didapatkan kembali - // misalnya ketika kembali dari halaman profil - // WidgetsBinding.instance.addPostFrameCallback((_) { - // final focusNode = FocusNode(); - // FocusScope.of(context).requestFocus(focusNode); - // focusNode.addListener(() { - // if (focusNode.hasFocus) { - // print('DEBUG WARGA: Halaman mendapatkan fokus, memuat ulang data'); - // controller.refreshData(); - // } - // }); - // }); - return Scaffold( key: scaffoldKey, appBar: AppBar( @@ -288,9 +275,6 @@ class WargaView extends GetView { activeIcon: Icons.report_problem, title: 'Pengaduan', isSelected: controller.activeTabIndex.value == 2, - badge: controller.totalPengaduanProses.value > 0 - ? controller.totalPengaduanProses.value.toString() - : null, onTap: () { Navigator.pop(context); controller.changeTab(2); diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 2dacbac..b4f2475 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -39,6 +39,7 @@ import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_peny import 'package:penyaluran_app/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart'; import 'package:penyaluran_app/app/modules/donatur/views/donatur_view.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; +import 'package:penyaluran_app/app/modules/donatur/views/donatur_jadwal_detail_view.dart'; part 'app_routes.dart'; @@ -239,5 +240,10 @@ class AppPages { }, binding: DonaturBinding(), ), + GetPage( + name: '/donatur/jadwal/:id', + page: () => const DonaturJadwalDetailView(), + binding: DonaturBinding(), + ), ]; } diff --git a/lib/app/utils/date_helper.dart b/lib/app/utils/date_helper.dart new file mode 100644 index 0000000..8edb839 --- /dev/null +++ b/lib/app/utils/date_helper.dart @@ -0,0 +1,43 @@ +import 'package:intl/intl.dart'; + +/// Kelas pembantu untuk manipulasi tanggal dan waktu +class DateHelper { + /// Format tanggal ke format Indonesia (dd MMM yyyy) + static String formatDate( + DateTime? dateTime, { + String format = 'dd MMM yyyy', + String locale = 'id_ID', + String defaultValue = 'Belum ditentukan', + }) { + if (dateTime == null) return defaultValue; + try { + return DateFormat(format, locale).format(dateTime.toLocal()); + } catch (e) { + return dateTime.toString().split(' ')[0]; + } + } + + /// Format nilai ke dalam format mata uang Rupiah + static String formatRupiah( + num? value, { + String symbol = 'Rp', + int decimalDigits = 0, + String defaultValue = 'Rp 0', + }) { + if (value == null) return defaultValue; + try { + final formatter = NumberFormat.currency( + locale: 'id_ID', + symbol: '$symbol ', + decimalDigits: decimalDigits, + ); + return formatter.format(value); + } catch (e) { + // Format manual + return '$symbol ${value.toStringAsFixed(decimalDigits).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + )}'; + } + } +} diff --git a/lib/app/utils/date_time_helper.dart b/lib/app/utils/format_helper.dart similarity index 87% rename from lib/app/utils/date_time_helper.dart rename to lib/app/utils/format_helper.dart index dd47fba..854bfbd 100644 --- a/lib/app/utils/date_time_helper.dart +++ b/lib/app/utils/format_helper.dart @@ -219,4 +219,34 @@ class DateTimeHelper { final localDateTime = toLocalDateTime(dateTime); return DateFormat('dd MMMM yyyy HH:mm', 'id_ID').format(localDateTime); } + + /// Format nilai ke dalam format mata uang Rupiah + /// + /// [value] adalah nilai yang akan diformat + /// [symbol] adalah simbol mata uang (default: 'Rp') + /// [decimalDigits] adalah jumlah digit desimal yang ditampilkan + /// [defaultValue] adalah nilai default jika value null + static String formatRupiah( + num? value, { + String symbol = 'Rp', + int decimalDigits = 0, + String defaultValue = 'Rp 0', + }) { + if (value == null) return defaultValue; + try { + final formatter = NumberFormat.currency( + locale: 'id_ID', + symbol: '$symbol ', + decimalDigits: decimalDigits, + ); + return formatter.format(value); + } catch (e) { + print('Error formatting currency: $e'); + // Fallback ke format manual + return '$symbol ${value.toStringAsFixed(decimalDigits).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + )}'; + } + } } diff --git a/lib/app/widgets/dialogs/detail_penitipan_dialog.dart b/lib/app/widgets/dialogs/detail_penitipan_dialog.dart index 94e9db2..9b0595b 100644 --- a/lib/app/widgets/dialogs/detail_penitipan_dialog.dart +++ b/lib/app/widgets/dialogs/detail_penitipan_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; -import 'package:penyaluran_app/app/utils/date_time_helper.dart'; +import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/theme/app_colors.dart'; /// Dialog untuk menampilkan detail penitipan bantuan