From 54c46603023205fbbc25dcf8fe98f5a83951bc2f Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Thu, 20 Mar 2025 05:19:04 +0700 Subject: [PATCH] Perbarui dependensi dan tambahkan fungsionalitas laporan penyaluran. Tambahkan paket baru seperti file_picker, pdf, dan open_file ke dalam pubspec.yaml. Hapus model LaporanModel yang tidak digunakan dan ganti dengan LaporanPenyaluranModel. Modifikasi tampilan dan controller untuk mendukung pengelolaan laporan penyaluran, termasuk navigasi dan ekspor ke PDF. Perbarui rute aplikasi untuk mencakup halaman laporan penyaluran baru. --- .../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 +- assets/font/DMSans-Bold.ttf | Bin 0 -> 56268 bytes assets/font/DMSans-Regular.ttf | Bin 0 -> 56348 bytes lib/app/data/models/laporan_model.dart | 89 -- .../data/models/laporan_penyaluran_model.dart | 61 + .../bindings/laporan_penyaluran_binding.dart | 11 + .../laporan_penyaluran_controller.dart | 1181 +++++++++++++++++ .../views/laporan_penyaluran_create_view.dart | 623 +++++++++ .../views/laporan_penyaluran_detail_view.dart | 1072 +++++++++++++++ .../views/laporan_penyaluran_edit_view.dart | 700 ++++++++++ .../views/laporan_penyaluran_view.dart | 410 ++++++ .../bindings/petugas_desa_binding.dart | 6 +- .../detail_penyaluran_controller.dart | 111 ++ .../controllers/laporan_controller.dart | 197 --- .../views/detail_penyaluran_page.dart | 218 ++- .../petugas_desa/views/petugas_desa_view.dart | 260 ++-- .../views/riwayat_penyaluran_view.dart | 2 +- lib/app/modules/warga/views/warga_view.dart | 5 + lib/app/routes/app_pages.dart | 25 + lib/app/widgets/custom_app_bar.dart | 4 +- lib/app/widgets/status_badge.dart | 31 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 116 +- pubspec.yaml | 14 + 29 files changed, 4702 insertions(+), 539 deletions(-) create mode 100644 assets/font/DMSans-Bold.ttf create mode 100644 assets/font/DMSans-Regular.ttf delete mode 100644 lib/app/data/models/laporan_model.dart create mode 100644 lib/app/data/models/laporan_penyaluran_model.dart create mode 100644 lib/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart create mode 100644 lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart create mode 100644 lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart create mode 100644 lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart create mode 100644 lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart create mode 100644 lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart delete mode 100644 lib/app/modules/petugas_desa/controllers/laporan_controller.dart diff --git a/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin b/android/app/.cxx/Debug/626b5o2n/arm64-v8a/configure_fingerprint.bin index 9fe891c..44c7df9 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 f43910d..37946ec 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 79fa7b2..c805443 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 1b52820..abe166b 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/assets/font/DMSans-Bold.ttf b/assets/font/DMSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4f5412dc8bb339e3a608597df2c4670d83dacc12 GIT binary patch literal 56268 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 fileUrls; // URL file laporan (PDF, Excel, dll) - final DateTime? tanggalMulai; - final DateTime? tanggalSelesai; - final DateTime? tanggalLaporan; - final DateTime? createdAt; - final DateTime? updatedAt; - - LaporanModel({ - this.id, - this.judul, - this.deskripsi, - this.jenis, - this.referensiId, - this.status, - this.petugasId, - this.fileUrls, - this.tanggalMulai, - this.tanggalSelesai, - this.tanggalLaporan, - this.createdAt, - this.updatedAt, - }); - - factory LaporanModel.fromRawJson(String str) => - LaporanModel.fromJson(json.decode(str)); - - String toRawJson() => json.encode(toJson()); - - factory LaporanModel.fromJson(Map json) => LaporanModel( - id: json["id"], - judul: json["judul"], - deskripsi: json["deskripsi"], - jenis: json["jenis"], - referensiId: json["referensi_id"], - status: json["status"], - petugasId: json["petugas_id"] ?? json["user_id"], - fileUrls: json["file_urls"] == null - ? null - : List.from(json["file_urls"].map((x) => x)), - tanggalMulai: json["tanggal_mulai"] != null - ? DateTime.parse(json["tanggal_mulai"]) - : json["periode_awal"] != null - ? DateTime.parse(json["periode_awal"]) - : null, - tanggalSelesai: json["tanggal_selesai"] != null - ? DateTime.parse(json["tanggal_selesai"]) - : json["periode_akhir"] != null - ? DateTime.parse(json["periode_akhir"]) - : null, - tanggalLaporan: json["tanggal_laporan"] != null - ? DateTime.parse(json["tanggal_laporan"]) - : null, - createdAt: json["created_at"] != null - ? DateTime.parse(json["created_at"]) - : null, - updatedAt: json["updated_at"] != null - ? DateTime.parse(json["updated_at"]) - : null, - ); - - Map toJson() => { - "id": id, - "judul": judul, - "deskripsi": deskripsi, - "jenis": jenis, - "referensi_id": referensiId, - "status": status, - "petugas_id": petugasId, - "file_urls": fileUrls == null - ? null - : List.from(fileUrls!.map((x) => x)), - "tanggal_mulai": tanggalMulai?.toIso8601String(), - "tanggal_selesai": tanggalSelesai?.toIso8601String(), - "tanggal_laporan": tanggalLaporan?.toIso8601String(), - "created_at": createdAt?.toIso8601String(), - "updated_at": updatedAt?.toIso8601String(), - }; -} diff --git a/lib/app/data/models/laporan_penyaluran_model.dart b/lib/app/data/models/laporan_penyaluran_model.dart new file mode 100644 index 0000000..db1a7bb --- /dev/null +++ b/lib/app/data/models/laporan_penyaluran_model.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +class LaporanPenyaluranModel { + final String? id; + final String penyaluranBantuanId; + final String judul; + final String? dokumentasiUrl; + final String? beritaAcaraUrl; + final DateTime? tanggalLaporan; + final String? status; + final DateTime? createdAt; + final DateTime? updatedAt; + + LaporanPenyaluranModel({ + this.id, + required this.penyaluranBantuanId, + required this.judul, + this.dokumentasiUrl, + this.beritaAcaraUrl, + this.tanggalLaporan, + this.status, + this.createdAt, + this.updatedAt, + }); + + factory LaporanPenyaluranModel.fromRawJson(String str) => + LaporanPenyaluranModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory LaporanPenyaluranModel.fromJson(Map json) => + LaporanPenyaluranModel( + id: json["id"], + penyaluranBantuanId: json["penyaluran_bantuan_id"], + judul: json["judul"], + dokumentasiUrl: json["dokumentasi_url"], + beritaAcaraUrl: json["berita_acara_url"], + tanggalLaporan: json["tanggal_laporan"] != null + ? DateTime.parse(json["tanggal_laporan"]).toUtc() + : null, + status: json["status"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]).toUtc() + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]).toUtc() + : null, + ); + + Map toJson() => { + "id": id, + "penyaluran_bantuan_id": penyaluranBantuanId, + "judul": judul, + "dokumentasi_url": dokumentasiUrl, + "berita_acara_url": beritaAcaraUrl, + "tanggal_laporan": tanggalLaporan?.toUtc().toIso8601String(), + "status": status, + "created_at": createdAt?.toUtc().toIso8601String(), + "updated_at": updatedAt?.toUtc().toIso8601String(), + }; +} diff --git a/lib/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart b/lib/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart new file mode 100644 index 0000000..d6e4ff0 --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; + +class LaporanPenyaluranBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => LaporanPenyaluranController(), + ); + } +} diff --git a/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart b/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart new file mode 100644 index 0000000..2c378d4 --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart @@ -0,0 +1,1181 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart'; +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/modules/auth/controllers/auth_controller.dart'; +import 'package:penyaluran_app/app/services/supabase_service.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +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'; + +class LaporanPenyaluranController extends GetxController { + final AuthController _authController = Get.find(); + final SupabaseService _supabaseService = SupabaseService.to; + + // Data + final RxList daftarLaporan = + [].obs; + final RxList penyaluranTanpaLaporan = + [].obs; + final Rx selectedLaporan = + Rx(null); + final Rx selectedPenyaluran = + Rx(null); + final RxList daftarPenerima = + [].obs; + final RxMap stokBantuanUsage = RxMap(); + final RxMap lokasiPenyaluran = RxMap(); + final RxMap desaData = RxMap(); + final RxMap kategoriBantuan = RxMap(); + + // Form controllers + final TextEditingController judulController = TextEditingController(); + + // File controllers untuk dokumentasi dan berita acara + final RxString dokumentasiPath = RxString(''); + final RxString beritaAcaraPath = RxString(''); + final RxBool isDokumentasiUploading = false.obs; + final RxBool isBeritaAcaraUploading = false.obs; + + // Loading states + final RxBool isLoading = false.obs; + final RxBool isSaving = false.obs; + final RxBool isExporting = false.obs; + + // Filter status + final RxString filterStatus = 'SEMUA'.obs; + + // Getter untuk data user + get user => _authController.user; + String get role => user?.role ?? 'WARGA'; + + @override + void onInit() { + super.onInit(); + fetchLaporan(); + } + + @override + void onClose() { + judulController.dispose(); + super.onClose(); + } + + // Reset form untuk pembuatan laporan baru + void resetForm() { + judulController.clear(); + dokumentasiPath.value = ''; + beritaAcaraPath.value = ''; + isDokumentasiUploading.value = false; + isBeritaAcaraUploading.value = false; + } + + // Mengambil data semua laporan penyaluran + Future fetchLaporan() async { + isLoading.value = true; + try { + final response = await _supabaseService.client + .from('laporan_penyaluran') + .select('*') + .order('created_at', ascending: false); + + daftarLaporan.value = (response as List) + .map((item) => LaporanPenyaluranModel.fromJson(item)) + .toList(); + + await fetchPenyaluranTanpaLaporan(); + } catch (e) { + print('Error fetching laporan: $e'); + Get.snackbar( + 'Error', + 'Gagal memuat data laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isLoading.value = false; + } + } + + // Mengambil data penyaluran yang belum memiliki laporan + Future fetchPenyaluranTanpaLaporan() async { + try { + // Ambil semua penyaluran dengan status TERLAKSANA + final response = await _supabaseService.client + .from('penyaluran_bantuan') + .select('*') + .eq('status', 'TERLAKSANA') + .order('tanggal_selesai', ascending: false); + + final allPenyaluran = (response as List) + .map((item) => PenyaluranBantuanModel.fromJson(item)) + .toList(); + + // Filter penyaluran yang belum memiliki laporan + final penyaluranIds = + daftarLaporan.map((e) => e.penyaluranBantuanId).toList(); + penyaluranTanpaLaporan.value = allPenyaluran + .where((penyaluran) => !penyaluranIds.contains(penyaluran.id)) + .toList(); + } catch (e) { + print('Error fetching penyaluran tanpa laporan: $e'); + } + } + + // Mendapatkan detail laporan berdasarkan ID + Future fetchLaporanDetail(String laporanId) async { + isLoading.value = true; + try { + final response = await _supabaseService.client + .from('laporan_penyaluran') + .select('*') + .eq('id', laporanId) + .single(); + + selectedLaporan.value = LaporanPenyaluranModel.fromJson(response); + + // Ambil data penyaluran terkait + await fetchPenyaluranDetail(selectedLaporan.value!.penyaluranBantuanId); + } catch (e) { + print('Error fetching laporan detail: $e'); + Get.snackbar( + 'Error', + 'Gagal memuat detail laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isLoading.value = false; + } + } + + // Mendapatkan detail penyaluran berdasarkan ID + Future fetchPenyaluranDetail(String penyaluranId) async { + try { + final response = await _supabaseService.client + .from('penyaluran_bantuan') + .select('*') + .eq('id', penyaluranId) + .single(); + + selectedPenyaluran.value = PenyaluranBantuanModel.fromJson(response); + + // Ambil data penerima terkait + await fetchPenerimaPenyaluran(penyaluranId); + + // Ambil data lokasi penyaluran + if (selectedPenyaluran.value?.lokasiPenyaluranId != null) { + await fetchLokasiPenyaluran( + selectedPenyaluran.value!.lokasiPenyaluranId!); + } + + // Hitung penggunaan stok bantuan + await calculateStokBantuanUsage(penyaluranId); + } catch (e) { + print('Error fetching penyaluran detail: $e'); + } + } + + // Mendapatkan daftar penerima penyaluran + Future fetchPenerimaPenyaluran(String penyaluranId) async { + try { + final response = await _supabaseService.client + .from('penerima_penyaluran') + .select('*, warga(*), stok_bantuan(*)') + .eq('penyaluran_bantuan_id', penyaluranId); + + daftarPenerima.value = (response as List) + .map((item) => PenerimaPenyaluranModel.fromJson(item)) + .toList(); + } catch (e) { + print('Error fetching penerima penyaluran: $e'); + } + } + + // Mendapatkan data lokasi penyaluran + Future fetchLokasiPenyaluran(String lokasiId) async { + try { + final response = await _supabaseService.client + .from('lokasi_penyaluran') + .select('*, desa_id') + .eq('id', lokasiId) + .single(); + + lokasiPenyaluran.value = response; + + // Ambil data desa jika ada desa_id + if (lokasiPenyaluran['desa_id'] != null) { + await fetchDesaData(lokasiPenyaluran['desa_id']); + } + } catch (e) { + print('Error fetching lokasi penyaluran: $e'); + } + } + + // Mendapatkan data desa + Future fetchDesaData(String desaId) async { + try { + final response = await _supabaseService.client + .from('desa') + .select('*, kecamatan, kabupaten, provinsi') + .eq('id', desaId) + .single(); + + desaData.value = response; + } catch (e) { + print('Error fetching desa data: $e'); + } + } + + // Mendapatkan data kategori bantuan berdasarkan ID + Future fetchKategoriBantuan(String kategoriId) async { + try { + final response = await _supabaseService.client + .from('kategori_bantuan') + .select('*') + .eq('id', kategoriId) + .single(); + + kategoriBantuan.value = response; + } catch (e) { + print('Error fetching kategori bantuan: $e'); + } + } + + // Menghitung penggunaan stok bantuan + Future calculateStokBantuanUsage(String penyaluranId) async { + try { + // Reset stok usage + stokBantuanUsage.clear(); + + // Group by stok_bantuan_id and calculate total usage + for (var penerima in daftarPenerima) { + if (penerima.stokBantuanId != null && penerima.jumlahBantuan != null) { + final stokId = penerima.stokBantuanId!; + final amount = penerima.jumlahBantuan!; + + if (stokBantuanUsage.containsKey(stokId)) { + stokBantuanUsage[stokId] = stokBantuanUsage[stokId]! + amount; + } else { + stokBantuanUsage[stokId] = amount; + } + + // Dapatkan kategori bantuan jika stok bantuan ini memilikinya + if (penerima.stokBantuan != null && + penerima.stokBantuan!['kategori_bantuan_id'] != null && + kategoriBantuan.isEmpty) { + await fetchKategoriBantuan( + penerima.stokBantuan!['kategori_bantuan_id']); + } + } + } + } catch (e) { + print('Error calculating stok bantuan usage: $e'); + } + } + + // Set form untuk edit + void setFormForEdit(LaporanPenyaluranModel laporan) { + judulController.text = laporan.judul; + + // Reset dokumentasi dan berita acara path agar tampil dari URL yang sudah ada + dokumentasiPath.value = ''; + beritaAcaraPath.value = ''; + } + + // Validasi form + bool validateForm() { + if (judulController.text.isEmpty) { + Get.snackbar( + 'Validasi Gagal', + 'Judul laporan tidak boleh kosong', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return false; + } + return true; + } + + // Upload dokumentasi + Future uploadDokumentasi(File file) async { + isDokumentasiUploading.value = true; + try { + final fileExt = file.path.split('.').last; + final fileName = + 'dokumentasi_${DateTime.now().millisecondsSinceEpoch}.$fileExt'; + final filePath = 'dokumentasi/$fileName'; + + final response = await _supabaseService.client.storage + .from('laporan_penyaluran') + .upload(filePath, file); + + // Dapatkan URL publik + final String publicUrl = _supabaseService.client.storage + .from('laporan_penyaluran') + .getPublicUrl(filePath); + + return publicUrl; + } catch (e) { + print('Error uploading dokumentasi: $e'); + Get.snackbar( + 'Error', + 'Gagal mengunggah dokumentasi', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return null; + } finally { + isDokumentasiUploading.value = false; + } + } + + // Upload berita acara + Future uploadBeritaAcara(File file) async { + isBeritaAcaraUploading.value = true; + try { + final fileExt = file.path.split('.').last; + final fileName = + 'berita_acara_${DateTime.now().millisecondsSinceEpoch}.$fileExt'; + final filePath = 'berita_acara/$fileName'; + + final response = await _supabaseService.client.storage + .from('laporan_penyaluran') + .upload(filePath, file); + + // Dapatkan URL publik + final String publicUrl = _supabaseService.client.storage + .from('laporan_penyaluran') + .getPublicUrl(filePath); + + return publicUrl; + } catch (e) { + print('Error uploading berita acara: $e'); + Get.snackbar( + 'Error', + 'Gagal mengunggah berita acara', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return null; + } finally { + isBeritaAcaraUploading.value = false; + } + } + + // Menyimpan laporan baru + Future saveLaporan(String penyaluranId) async { + if (!validateForm()) return; + + isSaving.value = true; + try { + // Upload dokumentasi jika ada + String? dokumentasiUrl; + if (dokumentasiPath.isNotEmpty) { + dokumentasiUrl = await uploadDokumentasi(File(dokumentasiPath.value)); + } + + // Upload berita acara jika ada + String? beritaAcaraUrl; + if (beritaAcaraPath.isNotEmpty) { + beritaAcaraUrl = await uploadBeritaAcara(File(beritaAcaraPath.value)); + } + + final data = { + 'penyaluran_bantuan_id': penyaluranId, + 'judul': judulController.text, + 'tanggal_laporan': DateTime.now().toUtc().toIso8601String(), + 'status': 'DRAFT', + if (dokumentasiUrl != null) 'dokumentasi_url': dokumentasiUrl, + if (beritaAcaraUrl != null) 'berita_acara_url': beritaAcaraUrl, + }; + + final response = await _supabaseService.client + .from('laporan_penyaluran') + .insert(data) + .select() + .single(); + + Get.snackbar( + 'Sukses', + 'Laporan berhasil disimpan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + + resetForm(); + Get.back(); + await fetchLaporan(); + } catch (e) { + print('Error saving laporan: $e'); + Get.snackbar( + 'Error', + 'Gagal menyimpan laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isSaving.value = false; + } + } + + // Update laporan + Future updateLaporan(String laporanId) async { + if (!validateForm()) return; + + isSaving.value = true; + try { + // Upload dokumentasi jika ada perubahan + String? dokumentasiUrl; + if (dokumentasiPath.isNotEmpty) { + dokumentasiUrl = await uploadDokumentasi(File(dokumentasiPath.value)); + } + + // Upload berita acara jika ada perubahan + String? beritaAcaraUrl; + if (beritaAcaraPath.isNotEmpty) { + beritaAcaraUrl = await uploadBeritaAcara(File(beritaAcaraPath.value)); + } + + final data = { + 'judul': judulController.text, + 'updated_at': DateTime.now().toUtc().toIso8601String(), + if (dokumentasiUrl != null) 'dokumentasi_url': dokumentasiUrl, + if (beritaAcaraUrl != null) 'berita_acara_url': beritaAcaraUrl, + }; + + await _supabaseService.client + .from('laporan_penyaluran') + .update(data) + .eq('id', laporanId); + + Get.snackbar( + 'Sukses', + 'Laporan berhasil diperbarui', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + + Get.back(); + await fetchLaporanDetail(laporanId); + } catch (e) { + print('Error updating laporan: $e'); + Get.snackbar( + 'Error', + 'Gagal memperbarui laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isSaving.value = false; + } + } + + // Finalisasi laporan + Future finalizeLaporan(String laporanId) async { + try { + await _supabaseService.client.from('laporan_penyaluran').update({ + 'status': 'FINAL', + 'updated_at': DateTime.now().toUtc().toIso8601String(), + }).eq('id', laporanId); + + Get.snackbar( + 'Sukses', + 'Laporan berhasil difinalisasi', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + + await fetchLaporanDetail(laporanId); + } catch (e) { + print('Error finalizing laporan: $e'); + Get.snackbar( + 'Error', + 'Gagal memfinalisasi laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Hapus laporan + Future deleteLaporan(String laporanId) async { + try { + await _supabaseService.client + .from('laporan_penyaluran') + .delete() + .eq('id', laporanId); + + Get.snackbar( + 'Sukses', + 'Laporan berhasil dihapus', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + + Get.back(); + await fetchLaporan(); + } catch (e) { + print('Error deleting laporan: $e'); + Get.snackbar( + 'Error', + 'Gagal menghapus laporan', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // Export laporan ke PDF + Future exportToPdf( + LaporanPenyaluranModel laporan, PenyaluranBantuanModel penyaluran) async { + isExporting.value = true; + try { + // Buat dokumen PDF + final pdf = pw.Document(); + + // Load font + final font = await rootBundle.load("assets/font/DMSans-Regular.ttf"); + final fontBold = await rootBundle.load("assets/font/DMSans-Bold.ttf"); + final ttf = pw.Font.ttf(font); + final ttfBold = pw.Font.ttf(fontBold); + + // Load logo - tidak perlu menampilkan error jika logo tidak ada + pw.MemoryImage? logoImage; + try { + final logoBytes = await rootBundle.load('assets/img/logo.png'); + logoImage = pw.MemoryImage(logoBytes.buffer.asUint8List()); + } catch (e) { + // Logo tidak ditemukan - tidak perlu print error + // Cukup terapkan null handling saat menggunakan logoImage + } + + // Coba unduh gambar dokumentasi menggunakan http statis (bukan dinamis) + pw.MemoryImage? dokumentasiImage; + if (laporan.dokumentasiUrl != null && + laporan.dokumentasiUrl!.isNotEmpty) { + try { + // Gunakan http package secara langsung (pastikan sudah ditambahkan di pubspec.yaml) + // import 'package:http/http.dart' as http; (tambahkan di bagian atas file) + final response = await http.get(Uri.parse(laporan.dokumentasiUrl!)); + if (response.statusCode == 200) { + dokumentasiImage = pw.MemoryImage(response.bodyBytes); + } + } catch (e) { + // Error unduh gambar - tidak perlu mencoba import dinamis + print('Tidak dapat mengunduh gambar dokumentasi: $e'); + } + } + + // Tambahkan halaman + pdf.addPage( + pw.MultiPage( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(20), + header: (pw.Context context) { + return pw.Column( + children: [ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + logoImage != null + ? pw.Image(logoImage, width: 60, height: 60) + : pw.SizedBox(width: 60), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Text('LAPORAN PENYALURAN BANTUAN', + style: pw.TextStyle( + font: ttfBold, + fontSize: 12, + color: PdfColors.blue900)), + pw.Text( + 'Tanggal: ${DateFormat('dd MMMM yyyy').format(DateTime.now())}', + style: pw.TextStyle(font: ttf, fontSize: 10), + ), + ], + ), + ], + ), + pw.SizedBox(height: 5), + pw.Divider(color: PdfColors.blue900, thickness: 2), + ], + ); + }, + footer: (pw.Context context) { + return pw.Column( + children: [ + pw.Divider(color: PdfColors.grey400), + pw.SizedBox(height: 5), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'Laporan Penyaluran | ${laporan.judul}', + style: pw.TextStyle( + font: ttf, fontSize: 8, color: PdfColors.grey600), + ), + pw.Text( + 'Halaman ${context.pageNumber} dari ${context.pagesCount}', + style: pw.TextStyle( + font: ttf, fontSize: 8, color: PdfColors.grey600), + ), + ], + ), + ], + ); + }, + build: (pw.Context context) { + return [ + // Judul Utama + pw.Center( + child: pw.Column( + children: [ + pw.Text( + laporan.judul.toUpperCase(), + style: pw.TextStyle( + font: ttfBold, + fontSize: 16, + color: PdfColors.blue900, + ), + textAlign: pw.TextAlign.center, + ), + pw.SizedBox(height: 5), + pw.Text( + penyaluran.nama ?? 'Penyaluran Bantuan', + style: pw.TextStyle( + font: ttf, + fontSize: 12, + color: PdfColors.grey700, + ), + textAlign: pw.TextAlign.center, + ), + ], + ), + ), + pw.SizedBox(height: 20), + + // Informasi Laporan + _buildPdfSection( + 'INFORMASI LAPORAN', + [ + _buildPdfRow('Judul Laporan', laporan.judul, ttf, ttfBold), + _buildPdfRow( + 'Tanggal Laporan', + laporan.tanggalLaporan != null + ? DateTimeHelper.formatDateTime( + laporan.tanggalLaporan!) + : '-', + ttf, + ttfBold), + _buildPdfRow( + 'Status', laporan.status ?? 'DRAFT', ttf, ttfBold), + ], + ttfBold, + PdfColors.blue900, + ), + + pw.SizedBox(height: 15), + + // Informasi Penyaluran + _buildPdfSection( + 'INFORMASI PENYALURAN', + [ + _buildPdfRow( + 'Nama Penyaluran', penyaluran.nama ?? '-', ttf, ttfBold), + _buildPdfRow( + 'Tanggal Penyaluran', + penyaluran.tanggalPenyaluran != null + ? DateTimeHelper.formatDateTime( + penyaluran.tanggalPenyaluran!) + : '-', + ttf, + ttfBold), + _buildPdfRow( + 'Tanggal Selesai', + penyaluran.tanggalSelesai != null + ? DateTimeHelper.formatDateTime( + penyaluran.tanggalSelesai!) + : '-', + ttf, + ttfBold), + _buildPdfRow('Jumlah Penerima', + '${penyaluran.jumlahPenerima ?? 0} orang', ttf, ttfBold), + _buildPdfRow('Status Penyaluran', penyaluran.status ?? '-', + ttf, ttfBold), + if (penyaluran.deskripsi != null && + penyaluran.deskripsi!.isNotEmpty) ...[ + pw.SizedBox(height: 10), + pw.Text('Deskripsi Penyaluran:', + style: pw.TextStyle(font: ttfBold)), + pw.SizedBox(height: 3), + pw.Container( + padding: const pw.EdgeInsets.all(8), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(4), + ), + child: pw.Text(penyaluran.deskripsi!, + style: pw.TextStyle(font: ttf, fontSize: 10)), + ), + ], + ], + ttfBold, + PdfColors.blue900, + ), + + // Informasi Lokasi Penyaluran + if (lokasiPenyaluran.isNotEmpty) ...[ + pw.SizedBox(height: 15), + _buildPdfSection( + 'LOKASI PENYALURAN', + [ + _buildPdfRow('Nama Lokasi', lokasiPenyaluran['nama'] ?? '-', + ttf, ttfBold), + _buildPdfRow( + 'Alamat', + lokasiPenyaluran['alamat_lengkap'] ?? '-', + ttf, + ttfBold), + if (desaData.isNotEmpty) ...[ + _buildPdfRow('Desa/Kelurahan', desaData['nama'] ?? '-', + ttf, ttfBold), + _buildPdfRow('Kecamatan', desaData['kecamatan'] ?? '-', + ttf, ttfBold), + _buildPdfRow('Kabupaten/Kota', + desaData['kabupaten'] ?? '-', ttf, ttfBold), + _buildPdfRow('Provinsi', desaData['provinsi'] ?? '-', ttf, + ttfBold), + ] else ...[ + _buildPdfRow('Kecamatan', + lokasiPenyaluran['kecamatan'] ?? '-', ttf, ttfBold), + _buildPdfRow( + 'Kelurahan/Desa', + lokasiPenyaluran['kelurahan_desa'] ?? '-', + ttf, + ttfBold), + ], + if (lokasiPenyaluran['keterangan'] != null && + lokasiPenyaluran['keterangan'] + .toString() + .isNotEmpty) ...[ + pw.SizedBox(height: 10), + pw.Text('Keterangan Lokasi:', + style: pw.TextStyle(font: ttfBold)), + pw.SizedBox(height: 3), + pw.Container( + padding: const pw.EdgeInsets.all(8), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + borderRadius: pw.BorderRadius.circular(4), + ), + child: pw.Text(lokasiPenyaluran['keterangan'], + style: pw.TextStyle(font: ttf, fontSize: 10)), + ), + ], + ], + ttfBold, + PdfColors.blue900, + ), + ], + + // Informasi Stok Bantuan + pw.SizedBox(height: 15), + pw.NewPage(), + if (stokBantuanUsage.isNotEmpty) ...[ + _buildPdfSection( + 'STOK BANTUAN YANG DIGUNAKAN', + [ + // Informasi kategori bantuan jika tersedia + if (kategoriBantuan.isNotEmpty) ...[ + pw.Container( + padding: const pw.EdgeInsets.all(8), + decoration: pw.BoxDecoration( + color: PdfColors.grey100, + border: pw.Border.all(color: PdfColors.grey300), + borderRadius: pw.BorderRadius.circular(5), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Kategori Bantuan: ${kategoriBantuan['nama'] ?? 'Tidak Diketahui'}', + style: pw.TextStyle( + font: ttfBold, + fontSize: 12, + ), + ), + if (kategoriBantuan['deskripsi'] != null) ...[ + pw.SizedBox(height: 3), + pw.Text( + kategoriBantuan['deskripsi'], + style: pw.TextStyle(font: ttf, fontSize: 10), + ), + ], + ], + ), + ), + pw.SizedBox(height: 10), + ], + pw.Table( + border: pw.TableBorder.all( + color: PdfColors.blue200, + width: 0.5, + ), + children: [ + // Header + pw.TableRow( + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + ), + children: [ + _buildPdfTableCell('Nama Bantuan', ttfBold, + isHeader: true, color: PdfColors.blue900), + _buildPdfTableCell('Jumlah Digunakan', ttfBold, + isHeader: true, + align: pw.TextAlign.center, + color: PdfColors.blue900), + ], + ), + // Data + ...stokBantuanUsage.entries.map((entry) { + final stokId = entry.key; + final jumlah = entry.value; + + // Find stok bantuan details + final stokBantuan = daftarPenerima + .firstWhere((p) => p.stokBantuanId == stokId, + orElse: () => PenerimaPenyaluranModel()) + ?.stokBantuan; + + if (stokBantuan == null) + return pw.TableRow(children: [ + _buildPdfTableCell('-', ttf), + _buildPdfTableCell('-', ttf), + ]); + + final isUang = stokBantuan['is_uang'] == true; + final formattedJumlah = isUang + ? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}' + : '$jumlah ${stokBantuan['satuan'] ?? ''}'; + + return pw.TableRow( + children: [ + _buildPdfTableCell( + stokBantuan['nama'] ?? '-', ttf), + _buildPdfTableCell(formattedJumlah, ttf, + align: pw.TextAlign.center), + ], + ); + }).toList(), + ], + ), + ], + ttfBold, + PdfColors.blue900, + ), + ], + + // Daftar Penerima + if (daftarPenerima.isNotEmpty) ...[ + pw.SizedBox(height: 15), + _buildPdfSection( + 'DAFTAR PENERIMA BANTUAN', + [ + pw.Table( + border: pw.TableBorder.all( + color: PdfColors.blue200, + width: 0.5, + ), + children: [ + // Header + pw.TableRow( + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + ), + children: [ + //nik + _buildPdfTableCell('NIK', ttfBold, + isHeader: true, color: PdfColors.blue900), + _buildPdfTableCell('Nama Penerima', ttfBold, + isHeader: true, color: PdfColors.blue900), + + _buildPdfTableCell('Jumlah', ttfBold, + isHeader: true, + align: pw.TextAlign.center, + color: PdfColors.blue900), + _buildPdfTableCell('Satuan', ttfBold, + isHeader: true, + align: pw.TextAlign.center, + color: PdfColors.blue900), + _buildPdfTableCell('Status', ttfBold, + isHeader: true, + align: pw.TextAlign.center, + color: PdfColors.blue900), + ], + ), + // Data + ...daftarPenerima.map((penerima) { + final nik = penerima.warga != null + ? penerima.warga!['nik'] ?? '-' + : '-'; + final wargaNama = penerima.warga != null + ? penerima.warga!['nama_lengkap'] ?? '-' + : '-'; + + final jumlah = penerima.jumlahBantuan != null + ? '${penerima.jumlahBantuan} ${penerima.satuan ?? ''}' + : '-'; + + return pw.TableRow( + children: [ + _buildPdfTableCell(nik, ttf), + _buildPdfTableCell(wargaNama, ttf), + _buildPdfTableCell(jumlah, ttf, + align: pw.TextAlign.center), + _buildPdfTableCell(penerima.satuan ?? '-', ttf, + align: pw.TextAlign.center), + _buildPdfTableCell( + penerima.statusPenerimaan ?? '-', ttf, + align: pw.TextAlign.center), + ], + ); + }).toList(), + ], + ), + ], + ttfBold, + PdfColors.blue900, + ), + ], + + // Dokumentasi & Berita Acara + if (laporan.dokumentasiUrl != null || + laporan.beritaAcaraUrl != null || + dokumentasiImage != null) ...[ + pw.SizedBox(height: 15), + _buildPdfSection( + 'DOKUMENTASI & BERITA ACARA', + [ + // // Tampilkan preview gambar dokumentasi jika berhasil diambil + // if (dokumentasiImage != null) ...[ + // pw.Text('Preview Dokumentasi:', + // style: pw.TextStyle( + // font: ttfBold, color: PdfColors.blue800)), + // pw.SizedBox(height: 5), + // pw.Center( + // child: pw.Container( + // decoration: pw.BoxDecoration( + // border: pw.Border.all(color: PdfColors.grey300), + // ), + // child: pw.Image(dokumentasiImage, height: 200), + // ), + // ), + // pw.SizedBox(height: 10), + // ], + + if (laporan.dokumentasiUrl != null) ...[ + if (dokumentasiImage == null) ...[ + pw.Text('Dokumentasi:', + style: pw.TextStyle( + font: ttfBold, color: PdfColors.blue800)), + pw.SizedBox(height: 3), + ], + pw.Text( + 'Dokumentasi dapat diakses melalui tautan berikut:', + style: pw.TextStyle(font: ttf)), + pw.SizedBox(height: 3), + pw.Text(laporan.dokumentasiUrl!, + style: pw.TextStyle( + font: ttf, + color: PdfColors.blue, + decoration: pw.TextDecoration.underline, + )), + if (laporan.beritaAcaraUrl != null) + pw.SizedBox(height: 10), + ], + + if (laporan.beritaAcaraUrl != null) ...[ + pw.Text('Berita Acara:', + style: pw.TextStyle( + font: ttfBold, color: PdfColors.blue800)), + pw.SizedBox(height: 3), + pw.Text( + 'Berita acara dapat diakses melalui tautan berikut:', + style: pw.TextStyle(font: ttf)), + pw.SizedBox(height: 3), + pw.Text(laporan.beritaAcaraUrl!, + style: pw.TextStyle( + font: ttf, + color: PdfColors.blue, + decoration: pw.TextDecoration.underline, + )), + ], + ], + ttfBold, + PdfColors.blue900, + ), + ], + + // Tanda Tangan + pw.SizedBox(height: 30), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.end, + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Text( + 'Penanggung Jawab,', + style: pw.TextStyle(font: ttf), + ), + pw.SizedBox(height: 50), // Ruang untuk tanda tangan + pw.Container( + width: 150, + decoration: pw.BoxDecoration( + border: pw.Border( + top: pw.BorderSide(color: PdfColors.black))), + padding: const pw.EdgeInsets.only(top: 5), + child: pw.Text( + // Sesuaikan dengan properti yang ada di model user + user?.email ?? 'Admin Sistem', + style: pw.TextStyle(font: ttfBold), + textAlign: pw.TextAlign.center, + ), + ), + ], + ), + ], + ), + ]; + }, + ), + ); + + // Simpan dan buka PDF + final output = await getTemporaryDirectory(); + final file = + File('${output.path}/laporan_penyaluran_${penyaluran.id}.pdf'); + await file.writeAsBytes(await pdf.save()); + await OpenFile.open(file.path); + + Get.snackbar( + 'Sukses', + 'Laporan berhasil diekspor ke PDF', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } catch (e) { + print('Error exporting to PDF: $e'); + Get.snackbar( + 'Error', + 'Gagal mengekspor laporan ke PDF', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } finally { + isExporting.value = false; + } + } + + // Helper untuk membuat section pada PDF + pw.Widget _buildPdfSection(String title, List content, + pw.Font titleFont, PdfColor titleColor) { + return pw.Container( + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.grey300), + borderRadius: pw.BorderRadius.circular(5), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Container( + padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: pw.BoxDecoration( + color: PdfColors.blue50, + border: + pw.Border(bottom: pw.BorderSide(color: PdfColors.grey300)), + ), + child: pw.Text( + title, + style: pw.TextStyle( + font: titleFont, fontSize: 14, color: titleColor), + ), + ), + pw.Container( + padding: const pw.EdgeInsets.all(10), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: content, + ), + ), + ], + ), + ); + } + + // Helper untuk membuat baris teks di PDF + pw.Widget _buildPdfRow( + String label, String value, pw.Font font, pw.Font boldFont) { + return pw.Padding( + padding: const pw.EdgeInsets.symmetric(vertical: 3), + child: pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.SizedBox( + width: 120, + child: pw.Text(label, + style: pw.TextStyle(font: boldFont, color: PdfColors.blue800)), + ), + pw.SizedBox(width: 10), + pw.Text(':', style: pw.TextStyle(font: boldFont)), + pw.SizedBox(width: 10), + pw.Expanded( + child: pw.Text(value, style: pw.TextStyle(font: font)), + ), + ], + ), + ); + } + + // Helper untuk membuat cell tabel di PDF + pw.Widget _buildPdfTableCell(String text, pw.Font font, + {bool isHeader = false, + pw.TextAlign align = pw.TextAlign.left, + PdfColor? color}) { + return pw.Padding( + padding: const pw.EdgeInsets.all(5), + child: pw.Text( + text, + style: pw.TextStyle( + font: font, + fontSize: isHeader ? 10 : 9, + color: color, + ), + textAlign: align, + ), + ); + } +} 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 new file mode 100644 index 0000000..0074a33 --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart @@ -0,0 +1,623 @@ +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/widgets/custom_app_bar.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; +import 'dart:io'; +import 'package:image_picker/image_picker.dart'; +import 'package:file_picker/file_picker.dart'; + +class LaporanPenyaluranCreateView extends GetView { + const LaporanPenyaluranCreateView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final penyaluranId = Get.arguments as String; + + // Dapatkan info penyaluran + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchPenyaluranDetail(penyaluranId); + controller.resetForm(); // Reset form setiap kali halaman dibuka + }); + + return Scaffold( + appBar: CustomAppBar( + title: 'Buat Laporan Penyaluran', + // subtitle: 'Isi form untuk membuat laporan penyaluran', + showBackButton: true, + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Informasi penyaluran + if (controller.selectedPenyaluran.value != null) ...[ + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Informasi Penyaluran', + // subtitle: 'Penyaluran yang akan dibuatkan laporan', + ), + const SizedBox(height: 16), + _buildInfoItem( + 'Nama Penyaluran', + controller.selectedPenyaluran.value!.nama ?? '-', + ), + _buildInfoItem( + 'Tanggal Penyaluran', + controller.selectedPenyaluran.value! + .tanggalPenyaluran != + null + ? DateTimeHelper.formatDateTime(controller + .selectedPenyaluran.value!.tanggalPenyaluran!) + : '-', + ), + _buildInfoItem( + 'Tanggal Selesai', + controller.selectedPenyaluran.value!.tanggalSelesai != + null + ? DateTimeHelper.formatDateTime(controller + .selectedPenyaluran.value!.tanggalSelesai!) + : '-', + ), + _buildInfoItem( + 'Jumlah Penerima', + '${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang', + ), + ], + ), + ), + ), + const SizedBox(height: 24), + ], + + // Form laporan + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Form Laporan', + ), + const SizedBox(height: 16), + + // Judul laporan + _buildTextField( + controller: controller.judulController, + label: 'Judul Laporan', + hint: 'Masukkan judul laporan', + required: true, + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Dokumentasi dan Berita Acara + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Dokumentasi & Berita Acara', + ), + const SizedBox(height: 16), + + // Upload Dokumentasi + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Dokumentasi', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.primaryColor, + ), + ), + ), + const SizedBox(height: 8), + Obx( + () => controller.dokumentasiPath.isNotEmpty + ? Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: + BorderRadius.circular(8), + child: Image.file( + File(controller + .dokumentasiPath.value), + width: 200, + height: 120, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + width: 200, + height: 120, + color: Colors.grey.shade200, + child: const Center( + child: Text( + 'Pratinjau tidak tersedia'), + ), + ), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () { + controller + .dokumentasiPath.value = ''; + }, + icon: Icon(Icons.delete, + color: AppTheme.errorColor), + label: Text('Hapus', + style: TextStyle( + color: + AppTheme.errorColor)), + ), + ], + ), + ], + ), + ) + : OutlinedButton.icon( + onPressed: () => + _pickDocumentationImage(context), + icon: const Icon(Icons.upload_file), + label: + const Text('Upload Foto Dokumentasi'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + side: BorderSide( + color: AppTheme.primaryColor), + ), + ), + ), + const SizedBox(height: 4), + Text( + 'Format gambar: JPG, PNG, JPEG (maks. 5MB)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Upload Berita Acara + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Berita Acara', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.primaryColor, + ), + ), + ), + const SizedBox(height: 8), + Obx( + () => controller.beritaAcaraPath.isNotEmpty + ? Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey.shade300), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.primaryColor + .withOpacity(0.1), + borderRadius: + BorderRadius.circular(8), + ), + child: Icon( + Icons.description, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Dokumen Berita Acara', + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 4), + Text( + controller.beritaAcaraPath.value + .split('/') + .last, + style: const TextStyle( + fontSize: 12), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + onPressed: () { + controller.beritaAcaraPath.value = + ''; + }, + icon: Icon(Icons.delete, + color: AppTheme.errorColor), + ), + ], + ), + ) + : OutlinedButton.icon( + onPressed: () => + _pickBeritaAcaraFile(context), + icon: const Icon(Icons.file_present), + label: const Text( + 'Upload Dokumen Berita Acara'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + side: BorderSide( + color: AppTheme.primaryColor), + ), + ), + ), + const SizedBox(height: 4), + Text( + 'Format file: PDF, DOC, DOCX (maks. 10MB)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 48), + + // Tombol simpan + Obx(() => SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: controller.isSaving.value + ? null + : () { + if (controller.judulController.text.isEmpty) { + Get.snackbar( + 'Perhatian', + 'Judul laporan wajib diisi', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: AppTheme.warningColor, + colorText: Colors.white, + ); + return; + } + controller.saveLaporan(penyaluranId); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: controller.isSaving.value + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ), + ) + : const Text( + 'Simpan Laporan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + )), + + const SizedBox(height: 32), + ], + ), + ); + }), + ); + } + + // Metode untuk memilih gambar dokumentasi + void _pickDocumentationImage(BuildContext context) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Pilih Sumber Gambar', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildImageSourceOption( + context, + Icons.camera_alt, + 'Kamera', + ImageSource.camera, + Colors.blue, + ), + _buildImageSourceOption( + context, + Icons.photo_library, + 'Galeri', + ImageSource.gallery, + AppTheme.primaryColor, + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + // Widget untuk opsi sumber gambar + Widget _buildImageSourceOption( + BuildContext context, + IconData icon, + String label, + ImageSource source, + Color color, + ) { + return InkWell( + onTap: () async { + Navigator.pop(context); + final ImagePicker picker = ImagePicker(); + try { + final XFile? image = await picker.pickImage( + source: source, + imageQuality: 80, + ); + if (image != null) { + controller.dokumentasiPath.value = image.path; + } + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal memilih gambar: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: AppTheme.errorColor, + colorText: Colors.white, + ); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Column( + children: [ + Icon(icon, size: 48, color: color), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } + + // Widget untuk memilih file berita acara + Future _pickBeritaAcaraFile(BuildContext context) async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf', 'doc', 'docx'], + ); + + if (result != null) { + controller.beritaAcaraPath.value = result.files.single.path!; + } + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal memilih file: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: AppTheme.errorColor, + colorText: Colors.white, + ); + } + } + + // Widget untuk item informasi + Widget _buildInfoItem(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 150, + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: AppTheme.primaryColor, + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } + + // Widget untuk input teks + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required String hint, + bool required = false, + int maxLines = 1, + bool isReadOnly = false, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.primaryColor, + ), + children: required + ? [ + TextSpan( + text: ' *', + style: TextStyle( + color: AppTheme.errorColor, + fontWeight: FontWeight.bold, + ), + ), + ] + : null, + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + maxLines: maxLines, + readOnly: isReadOnly, + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey[400]), + filled: true, + fillColor: Colors.grey[50], + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: AppTheme.primaryColor, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + contentPadding: const EdgeInsets.all(16), + ), + ), + ], + ), + ); + } +} 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 new file mode 100644 index 0000000..fdfbe7e --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart @@ -0,0 +1,1072 @@ +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/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'; +import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class LaporanPenyaluranDetailView extends GetView { + const LaporanPenyaluranDetailView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final laporanId = Get.arguments as String; + + // Ambil data detail laporan + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchLaporanDetail(laporanId); + }); + + return Scaffold( + appBar: CustomAppBar( + title: 'Detail Laporan', + // subtitle: 'Informasi lengkap tentang laporan penyaluran', + showBackButton: true, + actions: [ + Obx(() { + if (controller.isLoading.value) { + return const SizedBox.shrink(); + } + if (controller.selectedLaporan.value == null) { + return const SizedBox.shrink(); + } + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) => [ + if (controller.selectedLaporan.value!.status != 'FINAL') + PopupMenuItem( + value: 'finalize', + child: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.green), + SizedBox(width: 8), + Text('Finalisasi Laporan'), + ], + ), + ), + PopupMenuItem( + value: 'edit', + child: Row( + children: const [ + Icon(Icons.edit, color: Colors.orange), + SizedBox(width: 8), + Text('Edit Laporan'), + ], + ), + ), + PopupMenuItem( + value: 'export', + child: Obx(() => Row( + children: [ + controller.isExporting.value + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.blue, + ), + ) + : const Icon(Icons.download, color: Colors.blue), + const SizedBox(width: 8), + Text(controller.isExporting.value + ? 'Mengekspor...' + : 'Export ke PDF'), + ], + )), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: const [ + Icon(Icons.delete, color: Colors.red), + SizedBox(width: 8), + Text('Hapus Laporan'), + ], + ), + ), + ], + onSelected: (value) { + if (value == 'finalize') { + _showFinalizeConfirmation(context, laporanId); + } else if (value == 'edit') { + Get.toNamed('/laporan-penyaluran/edit', arguments: laporanId); + } else if (value == 'export') { + if (!controller.isExporting.value && + controller.selectedLaporan.value != null && + controller.selectedPenyaluran.value != null) { + controller.exportToPdf(controller.selectedLaporan.value!, + controller.selectedPenyaluran.value!); + } + } else if (value == 'delete') { + _showDeleteConfirmation(context, laporanId); + } + }, + ); + }), + ], + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.selectedLaporan.value == null) { + return const Center( + child: Text('Laporan tidak ditemukan'), + ); + } + + final laporan = controller.selectedLaporan.value!; + final penyaluran = controller.selectedPenyaluran.value; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status dan tanggal + _buildStatusHeader( + laporan.status ?? 'DRAFT', laporan.tanggalLaporan), + + const SizedBox(height: 16), + + // Informasi laporan + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Informasi Laporan', + ), + const SizedBox(height: 16), + _buildInfoRow( + 'Judul Laporan', + laporan.judul, + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Informasi penyaluran + if (penyaluran != null) + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Informasi Penyaluran', + // subtitle: 'Detail informasi penyaluran terkait', + ), + const SizedBox(height: 16), + _buildInfoRow( + 'Nama Penyaluran', penyaluran.nama ?? '-'), + _buildInfoRow( + 'Tanggal Penyaluran', + penyaluran.tanggalPenyaluran != null + ? DateFormat('dd/MM/yyyy') + .format(penyaluran.tanggalPenyaluran!) + : '-', + ), + _buildInfoRow( + 'Tanggal Selesai', + penyaluran.tanggalSelesai != null + ? DateFormat('dd/MM/yyyy') + .format(penyaluran.tanggalSelesai!) + : '-', + ), + _buildInfoRow('Jumlah Penerima', + '${penyaluran.jumlahPenerima ?? 0} orang'), + _buildInfoRow( + 'Status Penyaluran', penyaluran.status ?? '-'), + if (penyaluran.deskripsi != null && + penyaluran.deskripsi!.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text( + 'Deskripsi Penyaluran:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + penyaluran.deskripsi!, + style: const TextStyle(fontSize: 14), + ), + ], + ], + ), + ), + ), + + // Informasi Lokasi Penyaluran + if (controller.lokasiPenyaluran.isNotEmpty) + Column( + children: [ + const SizedBox(height: 24), + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Lokasi Penyaluran', + ), + const SizedBox(height: 16), + _buildInfoRow('Nama Lokasi', + controller.lokasiPenyaluran['nama'] ?? '-'), + _buildInfoRow( + 'Alamat', + controller.lokasiPenyaluran['alamat_lengkap'] ?? + '-'), + if (controller.desaData.isNotEmpty) ...[ + _buildInfoRow('Desa/Kelurahan', + controller.desaData['nama'] ?? '-'), + _buildInfoRow('Kecamatan', + controller.desaData['kecamatan'] ?? '-'), + _buildInfoRow('Kabupaten/Kota', + controller.desaData['kabupaten'] ?? '-'), + _buildInfoRow('Provinsi', + controller.desaData['provinsi'] ?? '-'), + ] else ...[ + _buildInfoRow( + 'Kecamatan', + controller.lokasiPenyaluran['kecamatan'] ?? + '-'), + _buildInfoRow( + 'Kabupaten/Kota', + controller + .lokasiPenyaluran['kabupaten_kota'] ?? + '-'), + _buildInfoRow( + 'Provinsi', + controller.lokasiPenyaluran['provinsi'] ?? + '-'), + ], + if (controller.lokasiPenyaluran['keterangan'] != + null && + controller.lokasiPenyaluran['keterangan'] + .toString() + .isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + 'Keterangan Lokasi:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + controller.lokasiPenyaluran['keterangan'], + style: const TextStyle(fontSize: 14), + ), + ], + ], + ), + ), + ), + ], + ), + + // Informasi Stok Bantuan yang Digunakan + if (controller.stokBantuanUsage.isNotEmpty) + Column( + children: [ + const SizedBox(height: 24), + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Stok Bantuan yang Digunakan', + ), + const SizedBox(height: 16), + + // Informasi kategori bantuan jika tersedia + if (controller.kategoriBantuan.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + border: + Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.category, + color: Colors.blue.shade700), + const SizedBox(width: 8), + Text( + 'Kategori Bantuan: ${controller.kategoriBantuan['nama'] ?? 'Tidak Diketahui'}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.blue.shade800, + ), + ), + ], + ), + if (controller + .kategoriBantuan['deskripsi'] != + null) ...[ + const SizedBox(height: 8), + Text( + controller.kategoriBantuan['deskripsi'], + style: TextStyle( + color: Colors.blue.shade700, + fontSize: 14, + ), + ), + ], + ], + ), + ), + const SizedBox(height: 16), + ], + + // Daftar stok bantuan + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.stokBantuanUsage.length, + itemBuilder: (context, index) { + final stokId = controller.stokBantuanUsage.keys + .elementAt(index); + final jumlah = + controller.stokBantuanUsage[stokId]; + + // Find stok bantuan details + final stokBantuan = controller.daftarPenerima + .firstWhere( + (p) => p.stokBantuanId == stokId, + orElse: () => PenerimaPenyaluranModel()) + .stokBantuan; + + if (stokBantuan == null) + return const SizedBox.shrink(); + + final kategori = + stokBantuan['kategori_bantuan'] != null + ? stokBantuan['kategori_bantuan'] + ['nama'] ?? + '-' + : '-'; + + final isUang = stokBantuan['is_uang'] == true; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Card( + color: isUang + ? Colors.green.shade50 + : Colors.grey.shade50, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + stokBantuan['nama'] ?? '-', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4), + decoration: BoxDecoration( + color: isUang + ? Colors.green.shade100 + : Colors.blue.shade100, + borderRadius: + BorderRadius.circular(12), + ), + child: Text( + kategori, + style: TextStyle( + fontSize: 12, + color: isUang + ? Colors.green.shade800 + : Colors.blue.shade800, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text( + 'Jumlah digunakan: ', + style: TextStyle(fontSize: 14), + ), + Text( + isUang + ? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}' + : '$jumlah ${stokBantuan['satuan'] ?? ''}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + if (stokBantuan['deskripsi'] != + null && + stokBantuan['deskripsi'] + .toString() + .isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + stokBantuan['deskripsi'], + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + + // Daftar Penerima Bantuan + if (controller.daftarPenerima.isNotEmpty) + Column( + children: [ + const SizedBox(height: 24), + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Daftar Penerima Bantuan', + ), + const SizedBox(height: 16), + // Header Tabel + Container( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: const [ + Expanded( + flex: 3, + child: Text( + 'Nama Penerima', + style: TextStyle( + fontWeight: FontWeight.bold), + ), + ), + Expanded( + flex: 3, + child: Text( + 'Jenis Bantuan', + style: TextStyle( + fontWeight: FontWeight.bold), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Jumlah', + style: TextStyle( + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + Expanded( + flex: 2, + child: Text( + 'Status', + style: TextStyle( + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + + // Baris Data Penerima + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.daftarPenerima.length, + itemBuilder: (context, index) { + final penerima = + controller.daftarPenerima[index]; + final wargaNama = penerima.warga != null + ? penerima.warga!['nama_lengkap'] ?? '-' + : '-'; + final stokNama = penerima.stokBantuan != null + ? penerima.stokBantuan!['nama'] ?? '-' + : '-'; + final jumlah = penerima.jumlahBantuan != null + ? '${penerima.jumlahBantuan} ${penerima.satuan ?? ''}' + : '-'; + + return Container( + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text(wargaNama), + ), + Expanded( + flex: 3, + child: Text(stokNama), + ), + Expanded( + flex: 2, + child: Text( + jumlah, + textAlign: TextAlign.center, + ), + ), + Expanded( + flex: 2, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor( + penerima.statusPenerimaan), + borderRadius: + BorderRadius.circular(12), + ), + child: Text( + penerima.statusPenerimaan ?? '-', + style: const TextStyle( + fontSize: 10, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + + // Dokumentasi dan Berita Acara + if (controller.selectedLaporan.value?.dokumentasiUrl != null || + controller.selectedLaporan.value?.beritaAcaraUrl != null) + Column( + children: [ + const SizedBox(height: 24), + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Dokumentasi & Berita Acara', + ), + const SizedBox(height: 16), + + // Dokumentasi + if (controller + .selectedLaporan.value?.dokumentasiUrl != + null) ...[ + const Text( + 'Dokumentasi:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + controller + .selectedLaporan.value!.dokumentasiUrl!, + width: double.infinity, + height: 200, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + Container( + width: double.infinity, + height: 50, + color: Colors.grey[200], + child: const Center( + child: Text('Gagal memuat gambar'), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: ElevatedButton.icon( + onPressed: () async { + final Uri url = Uri.parse(controller + .selectedLaporan + .value! + .dokumentasiUrl!); + if (!await launchUrl(url)) { + throw Exception( + 'Tidak dapat membuka $url'); + } + }, + icon: const Icon(Icons.image), + label: const Text('Lihat Dokumentasi'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + ), + ), + ), + ], + + if (controller.selectedLaporan.value + ?.dokumentasiUrl != + null && + controller.selectedLaporan.value + ?.beritaAcaraUrl != + null) + const SizedBox(height: 16), + + // Berita Acara + if (controller + .selectedLaporan.value?.beritaAcaraUrl != + null) ...[ + const Text( + 'Berita Acara:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + ListTile( + leading: const Icon( + Icons.description, + color: Colors.blue, + size: 40, + ), + title: const Text( + 'Dokumen Berita Acara', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: + const Text('Tap untuk membuka dokumen'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.grey.shade300), + ), + onTap: () async { + final Uri url = Uri.parse(controller + .selectedLaporan.value!.beritaAcaraUrl!); + if (!await launchUrl(url)) { + throw Exception('Tidak dapat membuka $url'); + } + }, + ), + ], + ], + ), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Tombol aksi + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (laporan.status != 'FINAL') + Expanded( + child: ElevatedButton.icon( + onPressed: () => + _showFinalizeConfirmation(context, laporanId), + icon: const Icon(Icons.check_circle), + label: const Text('Finalisasi'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + if (laporan.status != 'FINAL') const SizedBox(width: 12), + Expanded( + child: Obx(() => ElevatedButton.icon( + onPressed: controller.isExporting.value + ? null + : () { + if (controller.selectedLaporan.value != + null && + controller.selectedPenyaluran.value != + null) { + controller.exportToPdf( + controller.selectedLaporan.value!, + controller.selectedPenyaluran.value!); + } + }, + icon: controller.isExporting.value + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : const Icon(Icons.download), + label: Text(controller.isExporting.value + ? 'Mengekspor...' + : 'Export PDF'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(vertical: 12), + disabledBackgroundColor: + Colors.blue.withOpacity(0.7), + ), + )), + ), + ], + ), + ], + ), + ); + }), + ); + } + + // Membangun header status + Widget _buildStatusHeader(String status, DateTime? tanggalLaporan) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Laporan', + style: TextStyle( + fontSize: 14, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 6), + StatusBadge(status: status), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Tanggal Laporan', + style: TextStyle( + fontSize: 14, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 6), + Text( + tanggalLaporan != null + ? DateFormat('dd/MM/yyyy').format(tanggalLaporan) + : '-', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ); + } + + // Membangun baris informasi + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 150, + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: AppTheme.primaryColor, + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } + + // Helper untuk mendapatkan warna status penerimaan + Color _getStatusColor(String? status) { + if (status == null) return Colors.grey; + + switch (status.toUpperCase()) { + case 'DITERIMA': + return Colors.green; + case 'TERTUNDA': + return Colors.orange; + case 'DIBATALKAN': + return Colors.red; + case 'SEDANG DIPROSES': + return Colors.blue; + default: + return Colors.grey; + } + } + + // Menampilkan dokumen + Widget _buildDocumentSection(String judul, String? url, IconData icon) { + if (url == null || url.isEmpty) { + return const SizedBox.shrink(); + } + + return InkWell( + onTap: () async { + final Uri _url = Uri.parse(url); + if (await canLaunchUrl(_url)) { + await launchUrl(_url, mode: LaunchMode.externalApplication); + } else { + Get.snackbar( + 'Error', + 'Tidak dapat membuka dokumen', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: AppTheme.errorColor, + colorText: Colors.white, + ); + } + }, + child: Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.primaryColor.withOpacity(0.2), + ), + ), + child: Row( + children: [ + Icon( + icon, + color: AppTheme.primaryColor, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + judul, + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const SizedBox(height: 4), + const Text( + 'Tap untuk membuka dokumen', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + Icon( + Icons.open_in_new, + color: AppTheme.primaryColor, + size: 20, + ), + ], + ), + ), + ); + } + + // Dialog konfirmasi finalisasi laporan + void _showFinalizeConfirmation(BuildContext context, String laporanId) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Finalisasi Laporan'), + content: const Text( + 'Laporan yang sudah difinalisasi tidak dapat diubah lagi. Lanjutkan?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Batal', + style: TextStyle(color: AppTheme.primaryColor), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + ), + onPressed: () { + Navigator.of(context).pop(); + controller.finalizeLaporan(laporanId); + }, + child: const Text('Finalisasi'), + ), + ], + ); + }, + ); + } + + // Dialog konfirmasi hapus laporan + void _showDeleteConfirmation(BuildContext context, String laporanId) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Hapus Laporan'), + content: const Text( + 'Laporan yang dihapus tidak dapat dikembalikan. Lanjutkan?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Batal', + style: TextStyle(color: AppTheme.primaryColor), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.errorColor, + ), + onPressed: () { + Navigator.of(context).pop(); + controller.deleteLaporan(laporanId); + }, + child: const Text('Hapus'), + ), + ], + ); + }, + ); + } +} diff --git a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart new file mode 100644 index 0000000..65796bb --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart @@ -0,0 +1,700 @@ +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/widgets/custom_app_bar.dart'; +import 'package:penyaluran_app/app/widgets/section_header.dart'; +import 'dart:io'; +import 'package:image_picker/image_picker.dart'; +import 'package:file_picker/file_picker.dart'; + +class LaporanPenyaluranEditView extends GetView { + const LaporanPenyaluranEditView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final laporanId = Get.arguments as String; + + // Dapatkan data laporan + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchLaporanDetail(laporanId).then((_) { + if (controller.selectedLaporan.value != null) { + controller.setFormForEdit(controller.selectedLaporan.value!); + } + }); + }); + + return Scaffold( + appBar: CustomAppBar( + title: 'Edit Laporan Penyaluran', + // subtitle: 'Perbarui informasi laporan penyaluran', + showBackButton: true, + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.selectedLaporan.value == null) { + return const Center( + child: Text('Laporan tidak ditemukan'), + ); + } + + // Cek status laporan + if (controller.selectedLaporan.value!.status == 'FINAL') { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.lock, + size: 64, + color: Colors.orange, + ), + const SizedBox(height: 16), + const Text( + 'Laporan Telah Difinalisasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Laporan yang sudah difinalisasi tidak dapat diedit lagi.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Get.back(), + child: const Text('Kembali ke Detail Laporan'), + ), + ], + ), + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Informasi penyaluran + if (controller.selectedPenyaluran.value != null) ...[ + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Informasi Penyaluran', + // subtitle: 'Penyaluran yang terkait dengan laporan', + ), + const SizedBox(height: 16), + _buildInfoItem( + 'Nama Penyaluran', + controller.selectedPenyaluran.value!.nama ?? '-', + ), + _buildInfoItem( + 'Tanggal Penyaluran', + controller.selectedPenyaluran.value! + .tanggalPenyaluran != + null + ? '${controller.selectedPenyaluran.value!.tanggalPenyaluran!.day}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.month}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.year}' + : '-', + ), + _buildInfoItem( + 'Tanggal Selesai', + controller.selectedPenyaluran.value!.tanggalSelesai != + null + ? '${controller.selectedPenyaluran.value!.tanggalSelesai!.day}/${controller.selectedPenyaluran.value!.tanggalSelesai!.month}/${controller.selectedPenyaluran.value!.tanggalSelesai!.year}' + : '-', + ), + _buildInfoItem( + 'Jumlah Penerima', + '${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang', + ), + ], + ), + ), + ), + const SizedBox(height: 24), + ], + + // Form laporan + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Form Laporan', + ), + const SizedBox(height: 16), + + // Judul laporan + _buildTextField( + controller: controller.judulController, + label: 'Judul Laporan', + hint: 'Masukkan judul laporan', + required: true, + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Dokumentasi dan Berita Acara + Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Dokumentasi & Berita Acara', + ), + const SizedBox(height: 16), + + // Upload Dokumentasi + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Dokumentasi', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), + ), + ), + const SizedBox(height: 8), + + // Dokumentasi yang sudah ada + if (controller + .selectedLaporan.value?.dokumentasiUrl != + null && + controller.dokumentasiPath.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + controller.selectedLaporan.value! + .dokumentasiUrl!, + width: 200, + height: 120, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + width: 200, + height: 120, + color: Colors.grey.shade200, + child: const Center( + child: + Text('Pratinjau tidak tersedia'), + ), + ), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => + _pickDocumentationImage(context), + icon: const Icon(Icons.edit, + color: Colors.blue), + label: const Text('Ganti', + style: + TextStyle(color: Colors.blue)), + ), + ], + ), + ], + ), + ) + // Dokumentasi yang baru dipilih + else if (controller.dokumentasiPath.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(controller.dokumentasiPath.value), + width: 200, + height: 120, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + width: 200, + height: 120, + color: Colors.grey.shade200, + child: const Center( + child: + Text('Pratinjau tidak tersedia'), + ), + ), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () { + controller.dokumentasiPath.value = ''; + }, + icon: const Icon(Icons.delete, + color: Colors.red), + label: const Text('Hapus', + style: + TextStyle(color: Colors.red)), + ), + ], + ), + ], + ), + ) + // Tidak ada dokumentasi + else + OutlinedButton.icon( + onPressed: () => _pickDocumentationImage(context), + icon: const Icon(Icons.upload_file), + label: const Text('Upload Foto Dokumentasi'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + side: BorderSide(color: Colors.blue.shade300), + ), + ), + const SizedBox(height: 4), + Text( + 'Format gambar: JPG, PNG, JPEG (maks. 5MB)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Upload Berita Acara + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Berita Acara', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), + ), + ), + const SizedBox(height: 8), + + // Berita acara yang sudah ada + if (controller + .selectedLaporan.value?.beritaAcaraUrl != + null && + controller.beritaAcaraPath.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.description, + size: 40, + color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Dokumen Berita Acara', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + Uri.parse(controller + .selectedLaporan + .value! + .beritaAcaraUrl!) + .pathSegments + .last, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => + _pickBeritaAcaraFile(context), + icon: const Icon(Icons.edit, + color: Colors.blue), + label: const Text('Ganti', + style: + TextStyle(color: Colors.blue)), + ), + ], + ), + ], + ), + ) + // Berita acara yang baru dipilih + else if (controller.beritaAcaraPath.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.description, + size: 40, + color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + File(controller + .beritaAcaraPath.value) + .path + .split('/') + .last, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Dokumen berita acara', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () { + controller.beritaAcaraPath.value = ''; + }, + icon: const Icon(Icons.delete, + color: Colors.red), + label: const Text('Hapus', + style: + TextStyle(color: Colors.red)), + ), + ], + ), + ], + ), + ) + // Tidak ada berita acara + else + OutlinedButton.icon( + onPressed: () => _pickBeritaAcaraFile(context), + icon: const Icon(Icons.upload_file), + label: const Text('Upload Dokumen Berita Acara'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + side: BorderSide(color: Colors.blue.shade300), + ), + ), + const SizedBox(height: 4), + Text( + 'Format dokumen: PDF, DOC, DOCX (maks. 10MB)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 32), + + // Tombol aksi + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Get.back(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text('Batal'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Obx(() => ElevatedButton( + onPressed: controller.isSaving.value + ? null + : () => controller.updateLaporan(laporanId), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: controller.isSaving.value + ? const SizedBox( + height: 20, + width: 20, + child: + CircularProgressIndicator(strokeWidth: 2), + ) + : const Text( + 'Simpan Perubahan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + )), + ), + ], + ), + ], + ), + ); + }), + ); + } + + // Widget untuk item informasi + Widget _buildInfoItem(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + ), + ), + const Text(' : '), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } + + // Widget untuk input teks + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required String hint, + int maxLines = 1, + bool required = false, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[800], + ), + children: required + ? const [ + TextSpan( + text: ' *', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ] + : null, + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: hint, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + maxLines: maxLines, + ), + ], + ); + } + + // Metode untuk memilih gambar dokumentasi + Future _pickDocumentationImage(BuildContext context) async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + imageQuality: 80, + ); + + if (image != null) { + controller.dokumentasiPath.value = image.path; + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Terjadi kesalahan: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + // Metode untuk memilih file berita acara + Future _pickBeritaAcaraFile(BuildContext context) async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf', 'doc', 'docx'], + ); + + if (result != null) { + controller.beritaAcaraPath.value = result.files.single.path!; + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Terjadi kesalahan: $e'), + backgroundColor: Colors.red, + ), + ); + } + } +} diff --git a/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart new file mode 100644 index 0000000..d47f8bb --- /dev/null +++ b/lib/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart @@ -0,0 +1,410 @@ +import 'package:flutter/material.dart'; +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/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'; + +class LaporanPenyaluranView extends GetView { + const LaporanPenyaluranView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: 'Laporan Penyaluran Bantuan', + showBackButton: true, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Filter status + _buildStatusFilter(), + + // Daftar laporan + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + 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, + ), + ), + 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), + ), + ), + ], + ), + ); + } + + // Filter berdasarkan status jika dipilih + final filteredLaporan = controller.filterStatus.value == 'SEMUA' + ? controller.daftarLaporan + : controller.daftarLaporan + .where((laporan) => + laporan.status == controller.filterStatus.value) + .toList(); + + if (filteredLaporan.isEmpty) { + return Center( + child: Text( + 'Tidak ada laporan dengan status ${controller.filterStatus.value}'), + ); + } + + return RefreshIndicator( + onRefresh: () async { + await controller.fetchLaporan(); + }, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: filteredLaporan.length, + itemBuilder: (context, index) { + final laporan = filteredLaporan[index]; + return _buildLaporanCard(context, laporan); + }, + ), + ); + }), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showPenyaluranDialog(context), + child: const Icon(Icons.add), + ), + ); + } + + // Widget untuk filter status + Widget _buildStatusFilter() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + color: AppTheme.primaryColor.withOpacity(0.05), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionHeader( + title: 'Filter Status', + // subtitle: 'Tampilkan laporan berdasarkan status', + // showDivider: false, + ), + const SizedBox(height: 8), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterChip('SEMUA'), + _buildFilterChip('DRAFT'), + _buildFilterChip('FINAL'), + ], + ), + ), + ], + ), + ); + } + + // Chip untuk filter + Widget _buildFilterChip(String status) { + 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, + ), + ), + ); + }); + } + + // Widget untuk card laporan + Widget _buildLaporanCard( + BuildContext context, LaporanPenyaluranModel laporan) { + return Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + 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, + color: Colors.white, + ), + ), + ], + ), + 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), + onTap: () { + controller + .fetchPenyaluranDetail( + laporan.penyaluranBantuanId) + .then((_) { + if (controller.selectedPenyaluran.value != null) { + controller.exportToPdf(laporan, + controller.selectedPenyaluran.value!); + } + }); + }, + 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), + 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), + 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)), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + // Dialog konfirmasi hapus laporan + void _showDeleteConfirmation(BuildContext context, String laporanId) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Hapus Laporan'), + content: const Text('Apakah Anda yakin ingin menghapus laporan ini?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Batal'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + controller.deleteLaporan(laporanId); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Hapus'), + ), + ], + ); + }, + ); + } + + // Dialog pilih penyaluran untuk laporan baru + void _showPenyaluranDialog(BuildContext context) { + if (controller.penyaluranTanpaLaporan.isEmpty) { + Get.snackbar( + 'Info', + 'Tidak ada penyaluran yang tersedia untuk dibuat laporan. Pastikan ada penyaluran dengan status SELESAI.', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + return; + } + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Pilih Penyaluran'), + 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), + ), + subtitle: Text( + 'Tanggal: ${penyaluran.tanggalSelesai != null ? DateFormat('dd/MM/yyyy').format(penyaluran.tanggalSelesai!) : '-'}', + ), + onTap: () { + Navigator.of(context).pop(); + // Arahkan ke halaman buat laporan dengan ID penyaluran + Get.toNamed('/laporan-penyaluran/create', + arguments: penyaluran.id); + }, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Batal'), + ), + ], + ); + }, + ); + } +} diff --git a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart index 6b3c913..9131efa 100644 --- a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart +++ b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; @@ -6,7 +7,6 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart'; -import 'package:penyaluran_app/app/modules/petugas_desa/controllers/laporan_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart'; @@ -60,8 +60,8 @@ class PetugasDesaBinding extends Bindings { ); // Daftarkan controller laporan - Get.lazyPut( - () => LaporanController(), + Get.lazyPut( + () => LaporanPenyaluranController(), ); } } diff --git a/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart index 7505271..de1136e 100644 --- a/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart @@ -2,9 +2,11 @@ import 'package:get/get.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/data/models/penerima_penyaluran_model.dart'; +import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:flutter/material.dart'; import 'dart:io'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart'; class DetailPenyaluranController extends GetxController { final SupabaseService _supabaseService = Get.find(); @@ -14,10 +16,16 @@ class DetailPenyaluranController extends GetxController { final penyaluran = Rx(null); final skemaBantuan = Rx(null); final penerimaPenyaluran = [].obs; + final laporan = Rx(null); + final isLoadingLaporan = false.obs; // Status untuk mengetahui apakah petugas desa final isPetugasDesa = false.obs; + // Tambahkan referensi ke controller laporan + LaporanPenyaluranController? laporanController; + final RxBool isExporting = false.obs; + @override void onInit() { super.onInit(); @@ -432,6 +440,9 @@ class DetailPenyaluranController extends GetxController { } penerimaPenyaluran.assignAll(penerima); + // Periksa apakah ada laporan untuk penyaluran ini + await checkLaporanPenyaluran(penyaluranId); + // if (penerima.isNotEmpty) { // print('DetailPenyaluranController - ID penerima: ${penerima[0].id}'); // } @@ -446,6 +457,57 @@ class DetailPenyaluranController extends GetxController { } } + Future checkLaporanPenyaluran(String penyaluranId) async { + try { + isLoadingLaporan.value = true; + + final response = await _supabaseService.client + .from('laporan_penyaluran') + .select('*') + .eq('penyaluran_bantuan_id', penyaluranId) + .maybeSingle(); + + if (response != null) { + // Laporan ditemukan + laporan.value = LaporanPenyaluranModel.fromJson(response); + } else { + // Tidak ada laporan + laporan.value = null; + } + } catch (e) { + print('Error saat memeriksa laporan penyaluran: $e'); + laporan.value = null; + } finally { + isLoadingLaporan.value = false; + } + } + + void navigateToLaporanCreate() { + if (penyaluran.value?.id == null) return; + + // Kirim ID penyaluran langsung sebagai argument (String), bukan dalam bentuk Map + Get.toNamed('/laporan-penyaluran/create', arguments: penyaluran.value!.id) + ?.then((value) { + if (value == true) { + // Refresh data setelah membuat laporan + checkLaporanPenyaluran(penyaluran.value!.id!); + } + }); + } + + void navigateToLaporanDetail() { + if (laporan.value?.id == null) return; + + // Navigasi ke halaman detail dengan mengirimkan ID sebagai argument + Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.value!.id) + ?.then((value) { + if (value == true) { + // Refresh data setelah melihat detail laporan + checkLaporanPenyaluran(penyaluran.value!.id!); + } + }); + } + // Metode untuk verifikasi penerima berdasarkan QR code Future verifikasiPenerimaByQrCode( String penyaluranId, String qrHash) async { @@ -498,4 +560,53 @@ class DetailPenyaluranController extends GetxController { isProcessing.value = false; } } + + // Method untuk memuat controller laporan + Future loadLaporanPenyaluranController() async { + if (laporanController == null) { + // Cek apakah controller sudah ada di Get + if (Get.isRegistered()) { + laporanController = Get.find(); + } else { + // Jika belum ada, buat instance baru + laporanController = Get.put(LaporanPenyaluranController()); + } + } + + // Pastikan data laporan dimuat + if (laporan.value != null && penyaluran.value != null) { + await laporanController!.fetchLaporanDetail(laporan.value!.id!); + } + } + + // Method untuk export PDF + Future exportToPdf() async { + if (laporan.value == null || penyaluran.value == null) { + Get.snackbar( + 'Error', + 'Data laporan atau penyaluran tidak tersedia', + backgroundColor: Colors.red, + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + isExporting.value = true; + try { + await loadLaporanPenyaluranController(); + await laporanController!.exportToPdf(laporan.value!, penyaluran.value!); + } catch (e) { + print('Error saat export PDF: $e'); + Get.snackbar( + 'Error', + 'Gagal mengekspor laporan ke PDF', + backgroundColor: Colors.red, + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + isExporting.value = false; + } + } } diff --git a/lib/app/modules/petugas_desa/controllers/laporan_controller.dart b/lib/app/modules/petugas_desa/controllers/laporan_controller.dart deleted file mode 100644 index 8cb4bb2..0000000 --- a/lib/app/modules/petugas_desa/controllers/laporan_controller.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:penyaluran_app/app/data/models/laporan_model.dart'; -import 'package:penyaluran_app/app/data/models/user_model.dart'; -import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; -import 'package:penyaluran_app/app/services/supabase_service.dart'; - -class LaporanController extends GetxController { - final AuthController _authController = Get.find(); - final SupabaseService _supabaseService = SupabaseService.to; - - final RxBool isLoading = false.obs; - - // Indeks kategori yang dipilih untuk filter - final RxInt selectedCategoryIndex = 0.obs; - - // Data untuk laporan - final RxList daftarLaporan = [].obs; - - // Filter tanggal - final Rx tanggalMulai = Rx(null); - final Rx tanggalSelesai = Rx(null); - - // Controller untuk pencarian - final TextEditingController searchController = TextEditingController(); - - UserModel? get user => _authController.user; - - @override - void onInit() { - super.onInit(); - // Set default tanggal filter (1 bulan terakhir) - tanggalSelesai.value = DateTime.now(); - tanggalMulai.value = DateTime.now().subtract(const Duration(days: 30)); - loadLaporanData(); - } - - @override - void onClose() { - searchController.dispose(); - super.onClose(); - } - - Future loadLaporanData() async { - isLoading.value = true; - try { - final laporanData = await _supabaseService.getLaporan( - tanggalMulai.value, - tanggalSelesai.value, - ); - if (laporanData != null) { - daftarLaporan.value = - laporanData.map((data) => LaporanModel.fromJson(data)).toList(); - } - } catch (e) { - print('Error loading laporan data: $e'); - } finally { - isLoading.value = false; - } - } - - Future generateLaporan(String jenis) async { - isLoading.value = true; - try { - final laporan = LaporanModel( - jenis: jenis, - tanggalMulai: tanggalMulai.value, - tanggalSelesai: tanggalSelesai.value, - petugasId: user?.id, - createdAt: DateTime.now(), - ); - - final laporanId = - await _supabaseService.generateLaporan(laporan.toJson()); - - if (laporanId != null) { - await loadLaporanData(); - Get.snackbar( - 'Sukses', - 'Laporan berhasil dibuat', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } catch (e) { - print('Error generating laporan: $e'); - Get.snackbar( - 'Error', - 'Gagal membuat laporan: ${e.toString()}', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } finally { - isLoading.value = false; - } - } - - Future downloadLaporan(String laporanId) async { - isLoading.value = true; - try { - final url = await _supabaseService.downloadLaporan(laporanId); - if (url != null) { - // Implementasi download file - Get.snackbar( - 'Sukses', - 'Laporan berhasil diunduh', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } - } catch (e) { - print('Error downloading laporan: $e'); - Get.snackbar( - 'Error', - 'Gagal mengunduh laporan: ${e.toString()}', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } finally { - isLoading.value = false; - } - } - - Future deleteLaporan(String laporanId) async { - isLoading.value = true; - try { - await _supabaseService.deleteLaporan(laporanId); - await loadLaporanData(); - Get.snackbar( - 'Sukses', - 'Laporan berhasil dihapus', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } catch (e) { - print('Error deleting laporan: $e'); - Get.snackbar( - 'Error', - 'Gagal menghapus laporan: ${e.toString()}', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } finally { - isLoading.value = false; - } - } - - void setTanggalMulai(DateTime tanggal) { - tanggalMulai.value = tanggal; - } - - void setTanggalSelesai(DateTime tanggal) { - tanggalSelesai.value = tanggal; - } - - Future applyFilter() async { - await loadLaporanData(); - } - - Future refreshData() async { - isLoading.value = true; - try { - await loadLaporanData(); - } finally { - isLoading.value = false; - } - } - - void changeCategory(int index) { - selectedCategoryIndex.value = index; - } - - List getFilteredLaporan() { - switch (selectedCategoryIndex.value) { - case 0: - return daftarLaporan; - case 1: - return daftarLaporan - .where((item) => item.jenis == 'PENYALURAN') - .toList(); - case 2: - return daftarLaporan - .where((item) => item.jenis == 'STOK_BANTUAN') - .toList(); - case 3: - return daftarLaporan.where((item) => item.jenis == 'PENERIMA').toList(); - default: - return daftarLaporan; - } - } -} 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 4acbb3e..6af3e60 100644 --- a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart +++ b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart @@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/date_time_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:qr_flutter/qr_flutter.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart'; class DetailPenyaluranPage extends StatelessWidget { @@ -69,6 +68,9 @@ class DetailPenyaluranPage extends StatelessWidget { controller.penyaluran.value?.alasanPembatalan != null && controller.penyaluran.value!.alasanPembatalan!.isNotEmpty) _buildPembatalanSection(context), + if (controller.penyaluran.value?.status?.toUpperCase() == + 'TERLAKSANA') + _buildLaporanSection(context), const SizedBox(height: 16), _buildPenerimaPenyaluranSection(context), const SizedBox(height: 24), @@ -1495,6 +1497,220 @@ class DetailPenyaluranPage extends StatelessWidget { ); } + Widget _buildLaporanSection(BuildContext context) { + return Obx(() { + if (controller.isLoadingLaporan.value) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Container( + padding: const EdgeInsets.all(16), + child: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 8), + Text('Memuat data laporan...'), + ], + ), + ), + ), + ); + } + + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon( + Icons.description_outlined, + color: AppTheme.successColor, + size: 24, + ), + const SizedBox(width: 8), + const Text( + 'Laporan Penyaluran', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.successColor, + ), + ), + ], + ), + if (controller.laporan.value != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppTheme.successColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.successColor.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.check_circle, + color: AppTheme.successColor, + size: 16, + ), + const SizedBox(width: 4), + Text( + 'Tersedia', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppTheme.successColor, + ), + ), + ], + ), + ), + ], + ), + const Divider(height: 24), + if (controller.laporan.value == null) + Column( + children: [ + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.assignment_late_outlined, + size: 50, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Belum ada laporan penyaluran', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Buat laporan untuk mendokumentasikan hasil penyaluran bantuan', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: controller.navigateToLaporanCreate, + icon: const Icon(Icons.add_circle_outline), + label: const Text('Buat Laporan'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ], + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow('Judul', controller.laporan.value!.judul), + _buildInfoRow( + 'Tanggal Laporan', + controller.laporan.value?.tanggalLaporan != null + ? DateTimeHelper.formatDateTime( + controller.laporan.value!.tanggalLaporan!) + : '-', + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => + controller.navigateToLaporanDetail(), + icon: const Icon(Icons.visibility), + label: const Text('Lihat Detail'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.primaryColor, + side: const BorderSide( + color: AppTheme.primaryColor), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + if (controller.laporan.value?.beritaAcaraUrl != null && + controller + .laporan.value!.beritaAcaraUrl!.isNotEmpty) + Expanded( + child: Obx(() => ElevatedButton.icon( + onPressed: controller.isExporting.value + ? null + : () => controller.exportToPdf(), + icon: controller.isExporting.value + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + Colors.white), + ), + ) + : const Icon(Icons.download), + label: Text(controller.isExporting.value + ? 'Mengekspor...' + : 'Unduh PDF'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 12), + disabledBackgroundColor: + AppTheme.successColor.withOpacity(0.7), + ), + )), + ), + ], + ), + ], + ), + ], + ), + ), + ); + }); + } + List _getFilteredPenerima() { final query = searchQuery.value; final status = statusFilter.value; diff --git a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart index 942d4ca..f0c532b 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -179,18 +179,25 @@ class PetugasDesaView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, children: [ - const CircleAvatar( + CircleAvatar( radius: 30, backgroundColor: Colors.white, - child: Icon( - Icons.person, - size: 40, - color: AppTheme.primaryColor, - ), + backgroundImage: controller.user?.avatar != null && + controller.user!.avatar!.isNotEmpty + ? NetworkImage(controller.user!.avatar!) + : null, + child: controller.user?.avatar == null || + controller.user!.avatar!.isEmpty + ? const Icon( + Icons.person, + size: 40, + color: AppTheme.primaryColor, + ) + : null, ), const SizedBox(height: 10), Text( - controller.nama, + controller.user?.name ?? 'Petugas Desa', style: const TextStyle( color: Colors.white, fontSize: 18, @@ -198,203 +205,106 @@ class PetugasDesaView extends GetView { ), ), Text( - 'Petugas Desa', + controller.user?.desa?.nama != null + ? '${controller.user?.role} - ${controller.user!.desa!.nama}' + : controller.user?.role ?? 'PETUGAS_DESA', style: TextStyle( - color: Colors.white.withOpacity(0.8), + color: Colors.white.withAlpha(200), fontSize: 14, ), ), ], ), ), - Obx(() => ListTile( - leading: const Icon(Icons.dashboard_outlined), - title: const Text('Dashboard'), - selected: controller.activeTabIndex.value == 0, - selectedColor: AppTheme.primaryColor, - onTap: () { - controller.changeTab(0); - Navigator.pop(context); - }, - )), - Obx(() => ListTile( - leading: const Icon(Icons.calendar_today_outlined), - title: const Text('Penyaluran'), - selected: controller.activeTabIndex.value == 1, - selectedColor: AppTheme.primaryColor, - onTap: () { - controller.changeTab(1); - Navigator.pop(context); - }, - )), - Obx(() => ListTile( - leading: Stack( - alignment: Alignment.center, - children: [ - const Icon(Icons.handshake_outlined), - if (controller.jumlahMenunggu.value > 0) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 12, - minHeight: 12, - ), - child: Text( - controller.jumlahMenunggu.value.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 8, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - title: const Text('Penitipan'), - selected: controller.activeTabIndex.value == 2, - selectedColor: AppTheme.primaryColor, - onTap: () { - controller.changeTab(2); - Navigator.pop(context); - }, - )), - Obx(() { - final int jumlahPengaduanDiproses = controller.jumlahDiproses.value; - - return ListTile( - leading: Stack( - alignment: Alignment.center, - children: [ - const Icon(Icons.report_problem_outlined), - // Selalu tampilkan badge untuk debugging - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 12, - minHeight: 12, - ), - child: Text( - jumlahPengaduanDiproses.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 8, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - title: const Text('Pengaduan'), - selected: controller.activeTabIndex.value == 3, - selectedColor: AppTheme.primaryColor, - onTap: () { - controller.changeTab(3); - Navigator.pop(context); - }, - ); - }), - Obx(() => ListTile( - leading: const Icon(Icons.inventory_2_outlined), - title: const Text('Stok Bantuan'), - selected: controller.activeTabIndex.value == 4, - selectedColor: AppTheme.primaryColor, - onTap: () { - controller.changeTab(4); - Navigator.pop(context); - }, - )), - const Divider(), ListTile( - leading: const Icon(Icons.people_outline), - title: const Text('Daftar Penerima'), + leading: const Icon(Icons.dashboard_outlined), + title: const Text('Dashboard'), + selected: controller.activeTabIndex.value == 0, + selectedColor: AppTheme.primaryColor, onTap: () { - Navigator.pop(context); // Tutup drawer terlebih dahulu + Navigator.pop(context); + controller.changeTab(0); + }, + ), + ListTile( + leading: const Icon(Icons.handshake_outlined), + title: const Text('Penyaluran'), + selected: controller.activeTabIndex.value == 1, + selectedColor: AppTheme.primaryColor, + onTap: () { + Navigator.pop(context); + controller.changeTab(1); + }, + ), + ListTile( + leading: const Icon(Icons.inventory_2_outlined), + title: const Text('Penitipan'), + selected: controller.activeTabIndex.value == 2, + selectedColor: AppTheme.primaryColor, + onTap: () { + Navigator.pop(context); + controller.changeTab(2); + }, + ), + Obx(() => ListTile( + leading: controller.jumlahDiproses.value > 0 + ? Badge( + label: Text(controller.jumlahDiproses.value.toString()), + backgroundColor: Colors.red, + child: const Icon(Icons.support_outlined), + ) + : const Icon(Icons.support_outlined), + title: const Text('Pengaduan'), + selected: controller.activeTabIndex.value == 3, + selectedColor: AppTheme.primaryColor, + onTap: () { + Navigator.pop(context); + controller.changeTab(3); + }, + )), + ListTile( + leading: const Icon(Icons.inventory_outlined), + title: const Text('Stok Bantuan'), + selected: controller.activeTabIndex.value == 4, + selectedColor: AppTheme.primaryColor, + onTap: () { + Navigator.pop(context); + controller.changeTab(4); + }, + ), + ListTile( + leading: const Icon(Icons.person_add_outlined), + title: const Text('Kelola Penerima'), + onTap: () { + Navigator.pop(context); Get.toNamed('/daftar-penerima'); }, ), ListTile( - leading: const Icon(Icons.volunteer_activism_outlined), - title: const Text('Daftar Donatur'), + leading: const Icon(Icons.people_outlined), + title: const Text('Kelola Donatur'), onTap: () { - Navigator.pop(context); // Tutup drawer terlebih dahulu + Navigator.pop(context); Get.toNamed('/daftar-donatur'); }, ), ListTile( - leading: Stack( - alignment: Alignment.center, - children: [ - const Icon(Icons.notifications_outlined), - if (controller.jumlahNotifikasiBelumDibaca.value > 0) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(10), - ), - constraints: const BoxConstraints( - minWidth: 12, - minHeight: 12, - ), - child: Text( - controller.jumlahNotifikasiBelumDibaca.value.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 8, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - title: const Text('Notifikasi'), + leading: const Icon(Icons.description_outlined), + title: const Text('Laporan Penyaluran'), onTap: () { - Navigator.pop(context); // Tutup drawer terlebih dahulu - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NotifikasiView(), - ), - ); + Navigator.pop(context); + Get.toNamed('/laporan-penyaluran'); }, ), + const Divider(), ListTile( leading: const Icon(Icons.person_outline), title: const Text('Profil'), onTap: () { - // Navigasi ke halaman profil Navigator.pop(context); Get.toNamed('/profile'); }, ), - ListTile( - leading: const Icon(Icons.settings_outlined), - title: const Text('Pengaturan'), - onTap: () { - // Navigasi ke halaman pengaturan - Navigator.pop(context); - }, - ), ListTile( leading: const Icon(Icons.logout), title: const Text('Keluar'), 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 af5dc15..f54f486 100644 --- a/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart @@ -98,7 +98,7 @@ class RiwayatPenyaluranView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'terlaksana' : 'batal terlaksana'}', + 'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'Terlaksana' : 'Batal'}', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, diff --git a/lib/app/modules/warga/views/warga_view.dart b/lib/app/modules/warga/views/warga_view.dart index dd0db9f..c76e064 100644 --- a/lib/app/modules/warga/views/warga_view.dart +++ b/lib/app/modules/warga/views/warga_view.dart @@ -108,6 +108,11 @@ class WargaView extends GetView { badgeColor: Colors.orange, onTap: () => controller.changeTab(2), ), + DrawerMenuItem( + icon: Icons.description_outlined, + title: 'Laporan Penyaluran', + onTap: () => Get.toNamed('/laporan-penyaluran'), + ), ], )), body: Obx(() { diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 6b6f2dc..562f510 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -31,6 +31,11 @@ import 'package:penyaluran_app/app/modules/warga/views/warga_detail_penerimaan_v import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_pengaduan_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/pengaduan_binding.dart'; import 'package:penyaluran_app/app/modules/warga/views/detail_pengaduan_view.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart'; +import 'package:penyaluran_app/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart'; part 'app_routes.dart'; @@ -148,6 +153,26 @@ class AppPages { page: () => const RiwayatPengaduanView(), binding: RiwayatPengaduanBinding(), ), + GetPage( + name: _Paths.laporanPenyaluran, + page: () => const LaporanPenyaluranView(), + binding: LaporanPenyaluranBinding(), + ), + GetPage( + name: _Paths.laporanPenyaluran + '/detail', + page: () => const LaporanPenyaluranDetailView(), + binding: LaporanPenyaluranBinding(), + ), + GetPage( + name: _Paths.laporanPenyaluran + '/create', + page: () => const LaporanPenyaluranCreateView(), + binding: LaporanPenyaluranBinding(), + ), + GetPage( + name: _Paths.laporanPenyaluran + '/edit', + page: () => const LaporanPenyaluranEditView(), + binding: LaporanPenyaluranBinding(), + ), GetPage( name: _Paths.qrScanner, page: () => QrScannerPage(penyaluranId: Get.parameters['id'] ?? ''), diff --git a/lib/app/widgets/custom_app_bar.dart b/lib/app/widgets/custom_app_bar.dart index 6393438..913967f 100644 --- a/lib/app/widgets/custom_app_bar.dart +++ b/lib/app/widgets/custom_app_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:penyaluran_app/app/theme/app_colors.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; /// AppBar kustom yang digunakan di seluruh aplikasi /// @@ -60,7 +60,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { ), centerTitle: centerTitle, elevation: elevation, - backgroundColor: backgroundColor ?? AppColors.primary, + backgroundColor: backgroundColor ?? AppTheme.primaryColor, foregroundColor: foregroundColor ?? Colors.white, leading: _buildLeading(), actions: actions, diff --git a/lib/app/widgets/status_badge.dart b/lib/app/widgets/status_badge.dart index dff4d5f..3d59662 100644 --- a/lib/app/widgets/status_badge.dart +++ b/lib/app/widgets/status_badge.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; class StatusBadge extends StatelessWidget { final String status; @@ -20,20 +21,22 @@ class StatusBadge extends StatelessWidget { Widget build(BuildContext context) { final String statusUpper = status.toUpperCase(); - // Default colors for common statuses + // Default colors for common statuses menggunakan AppTheme final Map defaultColors = { - 'DITERIMA': Colors.green, - 'MENUNGGU': Colors.orange, - 'DITOLAK': Colors.red, - 'PROSES': Colors.orange, - 'DIPROSES': Colors.orange, - 'TINDAKAN': Colors.orange, - 'SELESAI': Colors.green, - 'TERVERIFIKASI': Colors.green, - 'BELUMMENERIMA': Colors.orange, - 'DIJADWALKAN': Colors.blue, - 'TERLAKSANA': Colors.purple, - 'AKTIF': Colors.green, + 'DITERIMA': AppTheme.verifiedColor, + 'MENUNGGU': AppTheme.processedColor, + 'DITOLAK': AppTheme.rejectedColor, + 'PROSES': AppTheme.processedColor, + 'DIPROSES': AppTheme.processedColor, + 'TINDAKAN': AppTheme.processedColor, + 'SELESAI': AppTheme.completedColor, + 'TERVERIFIKASI': AppTheme.verifiedColor, + 'BELUMMENERIMA': AppTheme.processedColor, + 'DIJADWALKAN': AppTheme.scheduledColor, + 'TERLAKSANA': AppTheme.completedColor, + 'AKTIF': AppTheme.verifiedColor, + 'DRAFT': AppTheme.processedColor, + 'FINAL': AppTheme.completedColor, }; // Default labels for common statuses @@ -50,6 +53,8 @@ class StatusBadge extends StatelessWidget { 'DIJADWALKAN': 'Dijadwalkan', 'TERLAKSANA': 'Terlaksana', 'AKTIF': 'Aktif', + 'DRAFT': 'Draft', + 'FINAL': 'Final', }; // Determine color and label based on status diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ea3bde6..2ea3ef0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 0420466..bc9a32d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_secure_storage_linux gtk + open_file_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4188def..8c2961c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,16 +6,20 @@ import FlutterMacOS import Foundation import app_links +import file_picker import file_selector_macos import flutter_secure_storage_macos +import open_file_mac import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2203e56..00ea691 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + url: "https://pub.dev" + source: hosted + version: "2.0.12" boolean_selector: dependency: transitive description: @@ -153,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b" + url: "https://pub.dev" + source: hosted + version: "9.2.1" file_selector_linux: dependency: transitive description: @@ -350,7 +374,7 @@ packages: source: hosted version: "2.1.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f @@ -533,6 +557,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e + url: "https://pub.dev" + source: hosted + version: "3.5.10" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://pub.dev" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://pub.dev" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://pub.dev" + source: hosted + version: "0.0.3" path: dependency: transitive description: @@ -550,7 +638,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -597,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.dev" + source: hosted + version: "3.11.3" petitparser: dependency: transitive description: @@ -814,34 +910,34 @@ packages: dependency: "direct main" description: name: syncfusion_flutter_calendar - sha256: "11b01bc7ad1d240d7c644081bda79e61c0a8d26eec7eba67bfc7274310562897" + sha256: "87761c6e73997d8d39181741f9e6244daf29b521aafaccf46b1bf31e86767a3f" url: "https://pub.dev" source: hosted - version: "28.2.11" + version: "28.2.12" syncfusion_flutter_core: dependency: transitive description: name: syncfusion_flutter_core - sha256: "59b6d2a7deacade6129d2f15615ca49ed56278fea055cd2e52cace78a343dd5e" + sha256: f1d2b52697543e13bdefdc62d15868124a265987577f53224a7dbe176c8448f0 url: "https://pub.dev" source: hosted - version: "28.2.11" + version: "28.2.12" syncfusion_flutter_datepicker: dependency: transitive description: name: syncfusion_flutter_datepicker - sha256: "73ece73742f123c750d674461c6902cbdf32fbd695c15fdf7e8487d290bb7179" + sha256: cfc91ebacee63b2c5220e541736f8df211d4f0bfbf34265778862ae20faf094f url: "https://pub.dev" source: hosted - version: "28.2.11+1" + version: "28.2.12" syncfusion_localizations: dependency: "direct main" description: name: syncfusion_localizations - sha256: "04bddcd326628ae3aea227d86534b1682e893674df3474fc71c83e0bffb27325" + sha256: "383f0bbd80cdd1bf37c72a4ff3fb0cb476f34e753dab4dd146ae57578fb1fa50" url: "https://pub.dev" source: hosted - version: "28.2.11" + version: "28.2.12" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 29f12b8..9dbb929 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ environment: dependencies: flutter: sdk: flutter + http: ^1.3.0 # atau versi yang lebih baru # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -63,6 +64,8 @@ dependencies: # Image picker untuk mengambil gambar dari kamera atau galeri image_picker: ^1.1.2 + # File picker untuk memilih file dokumen + file_picker: ^9.2.1 syncfusion_flutter_calendar: ^28.2.11 syncfusion_localizations: ^28.2.11 signature: ^6.0.0 @@ -81,6 +84,13 @@ dependencies: # Untuk fungsi hash crypto: ^3.0.3 + # Package untuk generate PDF + pdf: ^3.10.8 + # Package untuk menyimpan file + path_provider: ^2.1.2 + # Package untuk membuka file + open_file: ^3.3.2 + dev_dependencies: flutter_test: sdk: flutter @@ -124,6 +134,10 @@ flutter: - asset: assets/font/DMSans-VariableFont_opsz,wght.ttf - asset: assets/font/DMSans-Italic-VariableFont_opsz,wght.ttf style: italic + - asset: assets/font/DMSans-Regular.ttf + weight: 400 + - asset: assets/font/DMSans-Bold.ttf + weight: 700 # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf