From 0597f0aea049fc7706663bf83f8bb4bcf70e079d Mon Sep 17 00:00:00 2001 From: Khafidh Fuadi Date: Wed, 19 Mar 2025 13:11:24 +0700 Subject: [PATCH] Perbarui model dan tampilan untuk mendukung fungsionalitas QR code dalam proses verifikasi penerima. Tambahkan properti qrCodeHash pada PenerimaPenyaluranModel dan implementasikan metode verifikasi QR code di DetailPenyaluranController. Modifikasi tampilan di WargaDetailPenerimaanView dan DetailPenyaluranPage untuk menampilkan QR code dan menambahkan fungsionalitas pemindaian QR code. Perbarui rute aplikasi untuk mendukung navigasi ke halaman pemindaian QR code dan konfirmasi penerima. --- .vscode/settings.json | 3 + .../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 +- android/build.gradle | 5 +- .../models/penerima_penyaluran_model.dart | 4 + .../auth/controllers/auth_controller.dart | 18 +- .../detail_penyaluran_controller.dart | 53 +++++ .../jadwal_penyaluran_controller.dart | 42 +++- .../views/detail_penyaluran_page.dart | 224 +++++++++++------- .../views/konfirmasi_penerima_page.dart | 177 +++++++++----- .../petugas_desa/views/qr_scanner_page.dart | 196 +++++++++++++++ .../warga_dashboard_controller.dart | 13 +- .../views/warga_detail_penerimaan_view.dart | 195 +++++++++++---- lib/app/routes/app_pages.dart | 15 ++ lib/app/routes/app_routes.dart | 4 + pubspec.lock | 48 +++- pubspec.yaml | 9 + 19 files changed, 839 insertions(+), 263 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/app/modules/petugas_desa/views/qr_scanner_page.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dc3b895 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file 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 3902c56..36b5223 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  NJ2 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  NJ2  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  NJ2 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  NJ2 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  NJ2 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  NJ2y 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  NJ2 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  NJ2 ~ | -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  NJ2   -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  NJ2  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  NJ2  ( 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  NJ2  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 de14102..8112d56 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 d20ab28..7781258 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 bb50cf4..bf13839 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/android/build.gradle b/android/build.gradle index d2ffbff..f596659 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,6 +2,7 @@ allprojects { repositories { google() mavenCentral() + jcenter() } } @@ -10,7 +11,9 @@ subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(":app") + if (project.name.contains('qr_code_scanner_plus') || project.name.contains('flutter')) { + project.evaluationDependsOn(":app") + } } tasks.register("clean", Delete) { diff --git a/lib/app/data/models/penerima_penyaluran_model.dart b/lib/app/data/models/penerima_penyaluran_model.dart index 62bdfff..6264c56 100644 --- a/lib/app/data/models/penerima_penyaluran_model.dart +++ b/lib/app/data/models/penerima_penyaluran_model.dart @@ -22,6 +22,7 @@ class PenerimaPenyaluranModel { final String? deskripsiPenyaluran; // Deskripsi penyaluran final String? lokasiPenyaluranNama; // Nama lokasi penyaluran final String? lokasiPenyaluranAlamat; // Alamat lokasi penyaluran + final String? qrCodeHash; // Hash untuk QR code PenerimaPenyaluranModel({ this.id, @@ -45,6 +46,7 @@ class PenerimaPenyaluranModel { this.deskripsiPenyaluran, this.lokasiPenyaluranNama, this.lokasiPenyaluranAlamat, + this.qrCodeHash, }); factory PenerimaPenyaluranModel.fromRawJson(String str) => @@ -79,6 +81,7 @@ class PenerimaPenyaluranModel { deskripsiPenyaluran: json["deskripsi_penyaluran"], lokasiPenyaluranNama: json["lokasi_penyaluran_nama"], lokasiPenyaluranAlamat: json["lokasi_penyaluran_alamat"], + qrCodeHash: json["qr_code_hash"], ); Map toJson() => { @@ -103,5 +106,6 @@ class PenerimaPenyaluranModel { "deskripsi_penyaluran": deskripsiPenyaluran, "lokasi_penyaluran_nama": lokasiPenyaluranNama, "lokasi_penyaluran_alamat": lokasiPenyaluranAlamat, + "qr_code_hash": qrCodeHash, }; } diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart index f2186aa..92ecdb6 100644 --- a/lib/app/modules/auth/controllers/auth_controller.dart +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/providers/auth_provider.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; +import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; class AuthController extends GetxController { static AuthController get to => Get.find(); @@ -265,14 +266,29 @@ class AuthController extends GetxController { // Metode untuk logout Future logout() async { try { + // Ambil semua controller yang mungkin perlu dibersihkan + try { + final wargaController = Get.find(); + wargaController.penerimaPenyaluran.clear(); + wargaController.pengajuanKelayakan.clear(); + wargaController.pengaduan.clear(); + } catch (e) { + // Jika controller tidak ditemukan, abaikan + print('Controller tidak ditemukan: $e'); + } + + // Logout dari Supabase await _authProvider.signOut(); + + // Reset semua state _user.value = null; - _hasLoadedProfile.value = false; // Reset flag saat logout + _hasLoadedProfile.value = false; isWargaProfileComplete.value = false; // Bersihkan dependensi form sebelum navigasi clearFormDependencies(); + // Navigasi ke halaman login Get.offAllNamed(Routes.login); } catch (e) { Get.snackbar( 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 b720f8d..7505271 100644 --- a/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart @@ -445,4 +445,57 @@ class DetailPenyaluranController extends GetxController { isLoading.value = false; } } + + // Metode untuk verifikasi penerima berdasarkan QR code + Future verifikasiPenerimaByQrCode( + String penyaluranId, String qrHash) async { + try { + isProcessing.value = true; + + // Cari penerima dengan QR hash yang sesuai + final data = await _supabaseService.client + .from('penerima_penyaluran') + .select('*, warga:warga_id(*)') + .eq('penyaluran_bantuan_id', penyaluranId) + .eq('qr_code_hash', qrHash) + .single(); + + if (data != null) { + // Jika penerima ditemukan, konversi ke model + final Map sanitizedPenerimaData = + Map.from(data); + + // Konversi jumlah_bantuan ke double jika bertipe String + if (sanitizedPenerimaData['jumlah_bantuan'] is String) { + sanitizedPenerimaData['jumlah_bantuan'] = double.tryParse( + sanitizedPenerimaData['jumlah_bantuan'] as String); + } + + // Konversi data ke model + final penerima = + PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData); + + // Set isProcessing ke false sebelum navigasi untuk menghindari masalah loading + isProcessing.value = false; + + // Navigasi ke halaman konfirmasi dengan data terbaru + await Get.toNamed('/petugas-desa/konfirmasi-penerima/${penerima.id}', + arguments: { + 'penerima': penerima, + 'tanggal_penyaluran': penyaluran.value?.tanggalPenyaluran + }); + + // Refresh data + await refreshData(); + return true; + } + + return false; + } catch (e) { + print('Error verifikasi QR code: $e'); + return false; + } finally { + isProcessing.value = false; + } + } } diff --git a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart index bb34a47..9de570e 100644 --- a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -9,6 +9,8 @@ import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/utils/date_time_helper.dart'; import 'dart:async'; +import 'dart:convert'; +import 'package:crypto/crypto.dart'; class JadwalPenyaluranController extends GetxController { final AuthController _authController = Get.find(); @@ -415,38 +417,42 @@ class JadwalPenyaluranController extends GetxController { // Buat data penerima penyaluran untuk setiap pengajuan yang disetujui for (var pengajuan in pengajuanData) { + // Generate QR code hash unik untuk setiap penerima + final String qrCodeHash = + generateQrCodeHash(penyaluranId, pengajuan['warga_id']); + final penerimaPenyaluran = { 'penyaluran_bantuan_id': penyaluranId, 'warga_id': pengajuan['warga_id'], 'stok_bantuan_id': skemaBantuanCache[skemaId]?.stokBantuanId, - 'status_penerimaan': 'MENUNGGU', + 'status_penerimaan': 'BELUMMENERIMA', + 'qr_code_hash': qrCodeHash, }; + // Simpan data penerima ke database await _supabaseService.client .from('penerima_penyaluran') .insert(penerimaPenyaluran); } - // Refresh data + // Setelah berhasil menambahkan, refresh data await loadJadwalData(); + await loadPermintaanPenjadwalanData(); - // Kembali ke halaman sebelumnya - Get.back(); - - // Tampilkan notifikasi sukses + // Tampilkan pesan sukses Get.snackbar( 'Sukses', - 'Penyaluran berhasil ditambahkan', - snackPosition: SnackPosition.TOP, + 'Jadwal penyaluran bantuan telah dibuat', + snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, colorText: Colors.white, ); } catch (e) { - print('Error menambahkan penyaluran: $e'); + print('Error: $e'); Get.snackbar( - 'Error', - 'Gagal menambahkan penyaluran: ${e.toString()}', - snackPosition: SnackPosition.TOP, + 'Gagal', + 'Terjadi kesalahan saat menambahkan jadwal penyaluran', + snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, ); @@ -454,4 +460,16 @@ class JadwalPenyaluranController extends GetxController { isLoading.value = false; } } + + // Fungsi untuk generate hash QR code berdasarkan ID penyaluran dan ID warga + String generateQrCodeHash(String penyaluranId, String wargaId) { + // Kombinasikan ID penyaluran dan ID warga dengan timestamp untuk keunikan + final String combinedData = + '$penyaluranId-$wargaId-${DateTime.now().millisecondsSinceEpoch}'; + // Gunakan SHA-256 untuk menghasilkan hash yang aman + final bytes = utf8.encode(combinedData); + final hash = sha256.convert(bytes); + // Kembalikan representasi string dari hash + return hash.toString(); + } } 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 4d7501b..c874dfd 100644 --- a/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart +++ b/lib/app/modules/petugas_desa/views/detail_penyaluran_page.dart @@ -6,6 +6,8 @@ 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 { final controller = Get.put(DetailPenyaluranController()); @@ -75,20 +77,31 @@ class DetailPenyaluranPage extends StatelessWidget { ), ); }), - floatingActionButton: Obx(() => showScrollToTop.value - ? FloatingActionButton( - mini: true, - backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.arrow_upward), - onPressed: () { - scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - ) - : const SizedBox.shrink()), + floatingActionButton: Obx(() { + final status = controller.penyaluran.value?.status?.toUpperCase() ?? ''; + if (status == 'AKTIF') { + return FloatingActionButton( + backgroundColor: AppTheme.primaryColor, + onPressed: () => _showQrCodeScanner(context), + tooltip: 'Scan QR Code', + child: const Icon(Icons.qr_code_scanner, color: Colors.white), + ); + } + return showScrollToTop.value + ? FloatingActionButton( + mini: true, + backgroundColor: AppTheme.primaryColor, + child: const Icon(Icons.arrow_upward), + onPressed: () { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + ) + : const SizedBox.shrink(); + }), bottomNavigationBar: Obx(() { final status = controller.penyaluran.value?.status?.toUpperCase() ?? ''; if (status == 'AKTIF' || @@ -749,6 +762,18 @@ class DetailPenyaluranPage extends StatelessWidget { ); } + // Method untuk mendapatkan warna status + Color _getStatusColor(String status) { + switch (status.toUpperCase()) { + case 'DITERIMA': + return AppTheme.successColor; + case 'BELUMMENERIMA': + return AppTheme.warningColor; + default: + return Colors.grey; + } + } + Widget _buildPenerimaItem( BuildContext context, PenerimaPenyaluranModel item) { final warga = item.warga; @@ -848,52 +873,6 @@ class DetailPenyaluranPage extends StatelessWidget { ); } - Widget _buildStatusChipNew(String status) { - Color backgroundColor; - Color textColor = Colors.white; - String statusText = _getStatusPenerimaanText(status); - IconData iconData; - - // Konversi status ke format yang diinginkan - if (status.toUpperCase() == 'DITERIMA') { - backgroundColor = AppTheme.successColor; - statusText = 'Sudah Menerima'; - iconData = Icons.check_circle; - } else { - // Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA - backgroundColor = AppTheme.warningColor; - statusText = 'Belum Menerima'; - iconData = Icons.pending; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - iconData, - color: textColor, - size: 12, - ), - const SizedBox(width: 4), - Text( - statusText, - style: TextStyle( - color: textColor, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - Widget _buildStatusBadge(String status) { Color backgroundColor; Color textColor = Colors.white; @@ -1111,8 +1090,9 @@ class DetailPenyaluranPage extends StatelessWidget { ); } - void _showDetailPenerima( + void _showDetailPenerimaan( BuildContext context, PenerimaPenyaluranModel penerima) { + // Tampilkan detail penerimaan menggunakan bottom sheet final warga = penerima.warga; final bool sudahMenerima = penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA'; @@ -1125,7 +1105,7 @@ class DetailPenyaluranPage extends StatelessWidget { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) { + builder: (BuildContext context) { return Container( padding: const EdgeInsets.all(20), constraints: BoxConstraints( @@ -1185,7 +1165,26 @@ class DetailPenyaluranPage extends StatelessWidget { ), ), const SizedBox(height: 4), - _buildStatusChipNew(penerima.statusPenerimaan ?? '-'), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: statusColor.withOpacity(0.3)), + ), + child: Text( + sudahMenerima + ? 'Sudah Menerima' + : 'Belum Menerima', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ), ], ), ), @@ -1366,25 +1365,26 @@ class DetailPenyaluranPage extends StatelessWidget { // Tombol tutup SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey.shade200, - foregroundColor: Colors.black87, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade200, + foregroundColor: Colors.black87, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - child: const Text( - 'Tutup', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + ), + child: const Text( + 'Tutup', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), - )), + ), + ), + ), ], ), ), @@ -1527,4 +1527,68 @@ class DetailPenyaluranPage extends StatelessWidget { return filteredList; } + + // Fungsi untuk membuka scanner QR code + void _showQrCodeScanner(BuildContext context) async { + if (controller.penyaluran.value?.id == null) return; + + final result = await Get.to( + () => QrScannerPage( + penyaluranId: controller.penyaluran.value!.id!, + ), + ); + + if (result == true) { + // Refresh data setelah kembali dari scanner jika berhasil + await controller.refreshData(); + Get.snackbar( + 'Berhasil', + 'Penerima berhasil diverifikasi', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } + } + + // Widget untuk menampilkan QR Code (dikosongkan untuk petugas desa) + Widget _buildQrCodeSection(PenerimaPenyaluranModel penerima) { + // Widget QR Code tetap dibuat tapi tidak digunakan di petugas desa + return const SizedBox.shrink(); + } + + // Widget untuk status chip baru + Widget _buildStatusChipNew(String status) { + final Color statusColor; + final String statusText; + + if (status.toUpperCase() == 'DITERIMA') { + statusColor = AppTheme.successColor; + statusText = 'Sudah Menerima'; + } else { + statusColor = AppTheme.warningColor; + statusText = 'Belum Menerima'; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Text( + statusText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ); + } + + void _showDetailPenerima( + BuildContext context, PenerimaPenyaluranModel penerima) { + _showDetailPenerimaan(context, penerima); + } } diff --git a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart index 354e408..fbb5c01 100644 --- a/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart +++ b/lib/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart @@ -29,6 +29,7 @@ class KonfirmasiPenerimaPage extends StatefulWidget { } class _KonfirmasiPenerimaPageState extends State { + late PenerimaPenyaluranModel penerima; final controller = Get.find(); final ImagePicker _picker = ImagePicker(); File? _buktiPenerimaan; @@ -46,6 +47,16 @@ class _KonfirmasiPenerimaPageState extends State { // Untuk menyimpan gambar tanda tangan Uint8List? _signatureImage; + @override + void initState() { + super.initState(); + // Menggunakan data penerima yang diberikan dari arguments + penerima = widget.penerima; + print('KonfirmasiPenerimaPage - ID Penerima: ${penerima.id}'); + print( + 'KonfirmasiPenerimaPage - Nama Penerima: ${penerima.warga?['nama_lengkap']}'); + } + @override void dispose() { // Pastikan controller signature dibersihkan @@ -55,7 +66,7 @@ class _KonfirmasiPenerimaPageState extends State { @override Widget build(BuildContext context) { - final warga = widget.penerima.warga; + final warga = penerima.warga; return Scaffold( appBar: AppBar( @@ -66,40 +77,38 @@ class _KonfirmasiPenerimaPageState extends State { onPressed: () => Get.back(), ), ), - body: Obx( - () => controller.isProcessing.value || _isLoading - ? const Center( + body: _isLoading + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Sedang memproses konfirmasi...'), + ], + ), + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Sedang memproses konfirmasi...'), + _buildDetailPenerimaSection(warga), + const SizedBox(height: 16), + _buildDetailBantuanSection(), + const SizedBox(height: 16), + _buildFotoBuktiSection(), + const SizedBox(height: 16), + _buildTandaTanganSection(), + const SizedBox(height: 16), + _buildFormPersetujuanSection(), + const SizedBox(height: 24), + _buildKonfirmasiButton(), ], ), - ) - : SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailPenerimaSection(warga), - const SizedBox(height: 16), - _buildDetailBantuanSection(), - const SizedBox(height: 16), - _buildFotoBuktiSection(), - const SizedBox(height: 16), - _buildTandaTanganSection(), - const SizedBox(height: 16), - _buildFormPersetujuanSection(), - const SizedBox(height: 24), - _buildKonfirmasiButton(), - ], - ), - ), ), - ), + ), ); } @@ -156,6 +165,9 @@ class _KonfirmasiPenerimaPageState extends State { ), const Divider(), + //nama lengkap + _buildInfoRow('Nama Lengkap', warga?['nama_lengkap'] ?? 'Bajiyadi'), + // NIK _buildInfoRow('NIK', warga?['nik'] ?? '3201020107030010'), const Divider(), @@ -215,6 +227,8 @@ class _KonfirmasiPenerimaPageState extends State { String satuan = ''; if (widget.bentukBantuan?.satuan != null) { satuan = widget.bentukBantuan!.satuan!; + } else if (penerima.satuan != null) { + satuan = penerima.satuan!; } else { // Default satuan jika tidak ada satuan = 'Kg'; @@ -227,10 +241,35 @@ class _KonfirmasiPenerimaPageState extends State { final waktuSelesai = DateTimeHelper.formatTime( widget.tanggalPenyaluran!.add(const Duration(hours: 1))); tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai'; + } else if (penerima.penyaluranBantuan != null && + penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) { + final tanggalPenyaluran = + DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']); + final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran); + final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran); + final waktuSelesai = DateTimeHelper.formatTime( + tanggalPenyaluran.add(const Duration(hours: 1))); + tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai'; } else { tanggalWaktuPenyaluran = '09 April 2025 13:00-14:00'; } + // Ambil nama bantuan dari model jika tersedia + String namaBantuan = 'Beras'; + if (widget.bentukBantuan?.nama != null) { + namaBantuan = widget.bentukBantuan!.nama!; + } else if (penerima.kategoriNama != null) { + namaBantuan = penerima.kategoriNama!; + } + + // Ambil jumlah bantuan + String jumlahBantuan = '5'; + if (widget.jumlahBantuan != null) { + jumlahBantuan = widget.jumlahBantuan!; + } else if (penerima.jumlahBantuan != null) { + jumlahBantuan = penerima.jumlahBantuan.toString(); + } + return Card( elevation: 2, shape: RoundedRectangleBorder( @@ -252,13 +291,11 @@ class _KonfirmasiPenerimaPageState extends State { const SizedBox(height: 16), // Bentuk Bantuan - _buildInfoRow( - 'Bentuk Bantuan', widget.bentukBantuan?.nama ?? 'Beras'), + _buildInfoRow('Bentuk Bantuan', namaBantuan), const Divider(), // Nilai Bantuan - _buildInfoRow( - 'Nilai Bantuan', '${widget.jumlahBantuan ?? '5'}$satuan'), + _buildInfoRow('Nilai Bantuan', '$jumlahBantuan$satuan'), const Divider(), // Tanggal Penyaluran @@ -654,39 +691,62 @@ class _KonfirmasiPenerimaPageState extends State { File? signatureFile; try { - String imageUrl; - String signatureUrl; + String imageUrl = ''; + String signatureUrl = ''; // Upload bukti penerimaan - imageUrl = await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path); + try { + imageUrl = + await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path); + print('Berhasil upload bukti penerimaan: $imageUrl'); + } catch (e) { + // Jika upload bukti penerimaan gagal, tampilkan pesan dan hentikan proses + print('Error upload bukti penerimaan: $e'); + throw Exception('Gagal mengupload bukti penerimaan: $e'); + } // Simpan tanda tangan ke file sementara dan upload - tempDir = await Directory.systemTemp.createTemp('signature'); - signatureFile = File('${tempDir.path}/signature.png'); - await signatureFile.writeAsBytes(_signatureImage!); + try { + tempDir = await Directory.systemTemp.createTemp('signature'); + signatureFile = File('${tempDir.path}/signature.png'); + await signatureFile.writeAsBytes(_signatureImage!); - print('Signature file path: ${signatureFile.path}'); - print('Signature file exists: ${signatureFile.existsSync()}'); - print('Signature file size: ${signatureFile.lengthSync()} bytes'); + print('Signature file path: ${signatureFile.path}'); + print('Signature file exists: ${signatureFile.existsSync()}'); + print('Signature file size: ${signatureFile.lengthSync()} bytes'); - signatureUrl = await controller.uploadBuktiPenerimaan( - signatureFile.path, - isTandaTangan: true, - ); + signatureUrl = await controller.uploadBuktiPenerimaan( + signatureFile.path, + isTandaTangan: true, + ); + print('Berhasil upload tanda tangan: $signatureUrl'); + } catch (e) { + // Jika upload tanda tangan gagal, tampilkan pesan dan hentikan proses + print('Error upload tanda tangan: $e'); + throw Exception('Gagal mengupload tanda tangan: $e'); + } // Konfirmasi penerimaan - await controller.konfirmasiPenerimaan( - widget.penerima, - buktiPenerimaan: imageUrl, - tandaTangan: signatureUrl, - ); + try { + print('Melakukan konfirmasi penerimaan untuk ID: ${penerima.id}'); + await controller.konfirmasiPenerimaan( + penerima, + buktiPenerimaan: imageUrl, + tandaTangan: signatureUrl, + ); + print('Konfirmasi penerimaan berhasil'); + } catch (e) { + // Jika konfirmasi penerimaan gagal, tampilkan pesan dan hentikan proses + print('Error konfirmasi penerimaan: $e'); + throw Exception('Gagal melakukan konfirmasi penerimaan: $e'); + } // Hapus file sementara sebelum navigasi try { - if (signatureFile.existsSync()) { + if (signatureFile != null && signatureFile.existsSync()) { await signatureFile.delete(); } - if (tempDir.existsSync()) { + if (tempDir != null && tempDir.existsSync()) { await tempDir.delete(); } } catch (e) { @@ -736,9 +796,12 @@ class _KonfirmasiPenerimaPageState extends State { print('Error saat menghapus file sementara: $e'); } - setState(() { - _isLoading = false; - }); + // Pastikan state loading diatur kembali ke false + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } } diff --git a/lib/app/modules/petugas_desa/views/qr_scanner_page.dart b/lib/app/modules/petugas_desa/views/qr_scanner_page.dart new file mode 100644 index 0000000..6442d21 --- /dev/null +++ b/lib/app/modules/petugas_desa/views/qr_scanner_page.dart @@ -0,0 +1,196 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; +import 'package:penyaluran_app/app/theme/app_theme.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/controllers/detail_penyaluran_controller.dart'; + +class QrScannerPage extends StatefulWidget { + final String penyaluranId; + + const QrScannerPage({ + super.key, + required this.penyaluranId, + }); + + @override + State createState() => _QrScannerPageState(); +} + +class _QrScannerPageState extends State { + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + QRViewController? controller; + bool isScanning = true; + final DetailPenyaluranController detailController = + Get.find(); + bool isProcessing = false; + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan QR Code Penerima'), + backgroundColor: AppTheme.primaryColor, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Get.back(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller?.toggleFlash(); + }, + ), + IconButton( + icon: const Icon(Icons.flip_camera_ios), + onPressed: () async { + await controller?.flipCamera(); + }, + ), + ], + ), + body: Column( + children: [ + Expanded( + flex: 5, + child: Stack( + alignment: Alignment.center, + children: [ + QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: AppTheme.primaryColor, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: MediaQuery.of(context).size.width * 0.8, + ), + ), + // Tampilkan animasi loading saat memproses QR + if (isProcessing) + Container( + width: double.infinity, + height: double.infinity, + color: Colors.black45, + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + color: Colors.black, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Arahkan kamera ke QR Code penerima bantuan', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'QR Code akan otomatis terbaca', + style: TextStyle( + color: Colors.grey[400], + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + + void _onQRViewCreated(QRViewController controller) { + this.controller = controller; + controller.scannedDataStream.listen((scanData) async { + if (!isScanning || isProcessing) return; + + if (scanData.code != null) { + isScanning = false; + setState(() { + isProcessing = true; + }); + + try { + final qrHash = scanData.code!; + print('QR Hash yang terbaca: $qrHash'); + + final bool result = await detailController.verifikasiPenerimaByQrCode( + widget.penyaluranId, qrHash); + + if (result) { + // Success - Kembali ke halaman sebelumnya dengan hasil true + Get.back(result: true); + } else { + // QR Code tidak valid atau tidak ditemukan + Get.snackbar( + 'Gagal Memverifikasi', + 'QR Code tidak valid atau tidak terdaftar pada penyaluran ini', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + + // Lanjutkan pemindaian setelah delay + await Future.delayed(const Duration(seconds: 2)); + isScanning = true; + } + } catch (e) { + print('Error pemindaian QR: $e'); + Get.snackbar( + 'Error', + 'Terjadi kesalahan saat memproses QR code: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + + // Lanjutkan pemindaian setelah delay + await Future.delayed(const Duration(seconds: 2)); + isScanning = true; + } finally { + if (mounted) { + setState(() { + isProcessing = false; + }); + } + } + } + }); + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } +} diff --git a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart index af50e9f..161d022 100644 --- a/lib/app/modules/warga/controllers/warga_dashboard_controller.dart +++ b/lib/app/modules/warga/controllers/warga_dashboard_controller.dart @@ -85,8 +85,11 @@ class WargaDashboardController extends GetxController { } // Fungsi untuk mengambil data penerima penyaluran - Future fetchPenerimaPenyaluran() async { + Future> fetchPenerimaPenyaluran() async { try { + // Reset data terlebih dahulu untuk memastikan tidak ada data lama yang tersimpan + penerimaPenyaluran.clear(); + // Pertama, cari warga_id berdasarkan user_id final wargaResponse = await _supabaseService.client .from('warga') @@ -175,13 +178,21 @@ class WargaDashboardController extends GetxController { penerima.add(model); } + // Update nilai observable penerimaPenyaluran.assignAll(penerima); var diterima = penerima.where((p) => p.statusPenerimaan == 'DITERIMA').length; totalPenyaluranDiterima.value = diterima; + + // Log untuk debugging + print( + 'Berhasil memuat ${penerima.length} data penerimaan untuk warga ID: $wargaId'); + + return penerima; } catch (e) { print('Error fetchPenerimaPenyaluran: $e'); + return []; } } diff --git a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart index eb8db50..5f99b89 100644 --- a/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart +++ b/lib/app/modules/warga/views/warga_detail_penerimaan_view.dart @@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart'; +import 'package:qr_flutter/qr_flutter.dart'; class WargaDetailPenerimaanView extends GetView { const WargaDetailPenerimaanView({super.key}); @@ -14,6 +15,11 @@ class WargaDetailPenerimaanView extends GetView { final Map args = Get.arguments ?? {}; final id = args['id']; + // Segera muat ulang data penerimaan ketika halaman dibuka + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchPenerimaPenyaluran(); + }); + if (id == null) { return Scaffold( appBar: AppBar( @@ -28,64 +34,67 @@ class WargaDetailPenerimaanView extends GetView { // Konversi id ke string untuk memastikan kompatibilitas dengan model final String penyaluranId = id.toString(); - // Cari data penerimaan berdasarkan ID - final PenerimaPenyaluranModel? penyaluran = controller.penerimaPenyaluran - .firstWhereOrNull((item) => item.id == penyaluranId); + // Gunakan GetBuilder untuk memastikan widget dibangun ulang ketika data berubah + return Obx(() { + // Cari data penerimaan berdasarkan ID + final PenerimaPenyaluranModel? penyaluran = controller.penerimaPenyaluran + .firstWhereOrNull((item) => item.id == penyaluranId); + + if (penyaluran == null) { + return Scaffold( + appBar: AppBar( + title: const Text('Detail Penerimaan'), + ), + body: const Center( + child: Text('Data penerimaan tidak ditemukan'), + ), + ); + } + + final bool isDiterima = penyaluran.statusPenerimaan == 'DITERIMA'; - if (penyaluran == null) { return Scaffold( appBar: AppBar( title: const Text('Detail Penerimaan'), + elevation: 0, + backgroundColor: Get.theme.primaryColor, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Get.back(), + ), ), - body: const Center( - child: Text('Data penerimaan tidak ditemukan'), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Get.theme.primaryColor.withOpacity(0.05), + Colors.white, + ], + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderSection(penyaluran), + const SizedBox(height: 16), + _buildDetailSection(penyaluran), + const SizedBox(height: 16), + _buildLocationSection(penyaluran), + const SizedBox(height: 16), + if (isDiterima) _buildBuktiPenerimaanSection(penyaluran), + if (isDiterima) const SizedBox(height: 16), + _buildAdditionalInfoSection(penyaluran), + ], + ), + ), ), ); - } - - final bool isDiterima = penyaluran.statusPenerimaan == 'DITERIMA'; - - return Scaffold( - appBar: AppBar( - title: const Text('Detail Penerimaan'), - elevation: 0, - backgroundColor: Get.theme.primaryColor, - foregroundColor: Colors.white, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Get.back(), - ), - ), - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Get.theme.primaryColor.withOpacity(0.05), - Colors.white, - ], - ), - ), - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeaderSection(penyaluran), - const SizedBox(height: 16), - _buildDetailSection(penyaluran), - const SizedBox(height: 16), - _buildLocationSection(penyaluran), - const SizedBox(height: 16), - if (isDiterima) _buildBuktiPenerimaanSection(penyaluran), - if (isDiterima) const SizedBox(height: 16), - _buildAdditionalInfoSection(penyaluran), - ], - ), - ), - ), - ); + }); } Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) { @@ -577,12 +586,96 @@ class WargaDetailPenerimaanView extends GetView { .format(penyaluran.tanggalPenerimaan!) : 'Tidak tersedia', ), + + // Tambahkan QR Code untuk verifikasi + if (penyaluran.qrCodeHash != null && + penyaluran.qrCodeHash!.isNotEmpty) ...[ + const Divider(height: 24), + _buildQrCodeSection(penyaluran), + ], ], ), ), ); } + // Tambahkan widget untuk menampilkan QR Code + Widget _buildQrCodeSection(PenerimaPenyaluranModel penyaluran) { + // Pastikan menggunakan data terbaru dari model dan cetak ke log untuk debugging + final qrData = penyaluran.qrCodeHash ?? 'invalid-qr-code'; + print('QR Code Hash: $qrData'); // Log untuk debugging + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.qr_code, + color: Get.theme.primaryColor, + ), + const SizedBox(width: 8), + const Text( + 'QR Code Verifikasi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + Center( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Gunakan UniqueKey() untuk memaksa rebuild widget QR code + QrImageView( + key: UniqueKey(), + data: qrData, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, + errorStateBuilder: (cxt, err) { + return const Center( + child: Text( + "QR Code tidak tersedia", + textAlign: TextAlign.center, + ), + ); + }, + ), + const SizedBox(height: 12), + Text( + 'Tunjukkan QR Code ini kepada petugas untuk verifikasi penerimaan', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ); + } + Widget _buildDetailItem({ required IconData icon, required String title, diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 1b02b21..6b6f2dc 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -15,6 +15,8 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_ import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_pengaduan_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart'; +import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart'; @@ -146,5 +148,18 @@ class AppPages { page: () => const RiwayatPengaduanView(), binding: RiwayatPengaduanBinding(), ), + GetPage( + name: _Paths.qrScanner, + page: () => QrScannerPage(penyaluranId: Get.parameters['id'] ?? ''), + binding: PenyaluranBinding(), + ), + GetPage( + name: _Paths.konfirmasiPenerimaQr, + page: () => KonfirmasiPenerimaPage( + penerima: Get.arguments['penerima'], + tanggalPenyaluran: Get.arguments['tanggal_penyaluran'], + ), + binding: PenyaluranBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 759fc64..19cdcee 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -31,6 +31,8 @@ abstract class Routes { static const detailPengaduan = _Paths.detailPengaduan; static const wargaDetailPengaduan = _Paths.wargaDetailPengaduan; static const riwayatPengaduan = _Paths.riwayatPengaduan; + static const qrScanner = _Paths.qrScanner; + static const konfirmasiPenerimaQr = _Paths.konfirmasiPenerimaQr; } abstract class _Paths { @@ -64,4 +66,6 @@ abstract class _Paths { static const detailPengaduan = '/detail-pengaduan'; static const wargaDetailPengaduan = '/warga/detail-pengaduan'; static const riwayatPengaduan = '/petugas-desa/riwayat-pengaduan'; + static const qrScanner = '/petugas-desa/qr-scanner'; + static const konfirmasiPenerimaQr = '/petugas-desa/konfirmasi-penerima/:id'; } diff --git a/pubspec.lock b/pubspec.lock index 675ae16..2203e56 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -98,7 +98,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -409,10 +409,10 @@ packages: dependency: transitive description: name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_macos: dependency: transitive description: @@ -601,10 +601,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: @@ -637,6 +637,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_code_scanner_plus: + dependency: "direct main" + description: + name: qr_code_scanner_plus + sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" realtime_client: dependency: transitive description: @@ -859,7 +883,7 @@ packages: source: hosted version: "1.4.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" @@ -990,10 +1014,10 @@ packages: dependency: transitive description: name: win32 - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "5.12.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c26ce8..29f12b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,15 @@ dependencies: flutter_staggered_animations: ^1.1.1 timeline_tile: ^2.0.0 + # Library untuk QR code + qr_flutter: ^4.1.0 + qr_code_scanner_plus: ^2.0.10+1 + + # Untuk URL launcher + url_launcher: ^6.2.5 + # Untuk fungsi hash + crypto: ^3.0.3 + dev_dependencies: flutter_test: sdk: flutter