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.

This commit is contained in:
Khafidh Fuadi
2025-03-19 13:11:24 +07:00
parent 984b8336f0
commit 0597f0aea0
19 changed files with 839 additions and 263 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

@ -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  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  NJ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>

}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  <08><>ס<EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2~
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  NJ<EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  NJ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD>
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  NJ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
n
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  <08><>ס<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja  NJ<EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <08><>ס<EFBFBD>2y
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  NJ<EFBFBD><EFBFBD><EFBFBD>2y
w
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  <08><>ס<EFBFBD>2
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  NJ<EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
x
x
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  NJ<><C78A><EFBFBD>2 ~
|
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  NJ<><C78A><EFBFBD>2
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  NJ<><C78A><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2w
u
u
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  NJ<><C78A><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2|
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2|

View File

@ -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  ɠס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD>
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt  ɠס<EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
~
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json  ɠס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD>
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json  ʠס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
p
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja  ʠס<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
t
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  ʠס<EFBFBD>2{
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  ʠס<EFBFBD>2
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2|
z
z
xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 <09>
~
~
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin  <08><><EFBFBD><EFBFBD><EFBFBD>2
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2y
w
w
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json  <08><><EFBFBD><EFBFBD><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2~
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2~

View File

@ -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  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  <08><>ס<EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2x
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2x
v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
{
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  <08><>ס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
h
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  <08><>ס<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  <08><><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
l
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  <08><>ס<EFBFBD>2s
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2s
q
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  <08><>ס<EFBFBD>2
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2t
r
r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 x
v
v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  <08><><EFBFBD><EFBFBD><EFBFBD>2
~
|
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
o
o
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json  <08><><EFBFBD><EFBFBD><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2v
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2v

View File

@ -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  Ԣס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  Ԣס<EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2{
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  Ԣס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
~
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  Ԣס<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
k
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  Ԣס<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
o
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  Ԣס<EFBFBD>2v
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2v
t
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  Ԣס<EFBFBD>2
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2w
u
u
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 {
y
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  <08><><EFBFBD><EFBFBD><EFBFBD>2
<EFBFBD>


}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2t
r
r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  <08><><EFBFBD><EFBFBD><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2y
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2y

View File

@ -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) {

View File

@ -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<String, dynamic> toJson() => {
@ -103,5 +106,6 @@ class PenerimaPenyaluranModel {
"deskripsi_penyaluran": deskripsiPenyaluran,
"lokasi_penyaluran_nama": lokasiPenyaluranNama,
"lokasi_penyaluran_alamat": lokasiPenyaluranAlamat,
"qr_code_hash": qrCodeHash,
};
}

View File

@ -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<void> logout() async {
try {
// Ambil semua controller yang mungkin perlu dibersihkan
try {
final wargaController = Get.find<WargaDashboardController>();
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(

View File

@ -445,4 +445,57 @@ class DetailPenyaluranController extends GetxController {
isLoading.value = false;
}
}
// Metode untuk verifikasi penerima berdasarkan QR code
Future<bool> 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<String, dynamic> sanitizedPenerimaData =
Map<String, dynamic>.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;
}
}
}

View File

@ -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<AuthController>();
@ -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();
}
}

View File

@ -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);
}
}

View File

@ -29,6 +29,7 @@ class KonfirmasiPenerimaPage extends StatefulWidget {
}
class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
late PenerimaPenyaluranModel penerima;
final controller = Get.find<DetailPenyaluranController>();
final ImagePicker _picker = ImagePicker();
File? _buktiPenerimaan;
@ -46,6 +47,16 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
// 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<KonfirmasiPenerimaPage> {
@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<KonfirmasiPenerimaPage> {
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<KonfirmasiPenerimaPage> {
),
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<KonfirmasiPenerimaPage> {
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<KonfirmasiPenerimaPage> {
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<KonfirmasiPenerimaPage> {
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<KonfirmasiPenerimaPage> {
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<KonfirmasiPenerimaPage> {
print('Error saat menghapus file sementara: $e');
}
setState(() {
_isLoading = false;
});
// Pastikan state loading diatur kembali ke false
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}

View File

@ -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<QrScannerPage> createState() => _QrScannerPageState();
}
class _QrScannerPageState extends State<QrScannerPage> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? controller;
bool isScanning = true;
final DetailPenyaluranController detailController =
Get.find<DetailPenyaluranController>();
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();
}
}

View File

@ -85,8 +85,11 @@ class WargaDashboardController extends GetxController {
}
// Fungsi untuk mengambil data penerima penyaluran
Future<void> fetchPenerimaPenyaluran() async {
Future<List<PenerimaPenyaluranModel>> 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 [];
}
}

View File

@ -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<WargaDashboardController> {
const WargaDetailPenerimaanView({super.key});
@ -14,6 +15,11 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
final Map<String, dynamic> 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<WargaDashboardController> {
// 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<WargaDashboardController> {
.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,

View File

@ -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(),
),
];
}

View File

@ -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';
}

View File

@ -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:

View File

@ -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