Perbarui model dan tampilan untuk mendukung status penyaluran dalam aplikasi. Tambahkan properti statusPenyaluran pada PenerimaPenyaluranModel dan SkemaBantuanModel. Modifikasi tampilan di BantuanCard dan StatusBadge untuk menampilkan status penyaluran dengan lebih baik. Hapus penggunaan prioritas di beberapa model dan tampilan untuk menyederhanakan kode. Implementasikan logika baru di JadwalPenyaluranController untuk memperbarui stok bantuan berdasarkan jumlah yang diterima.

This commit is contained in:
Khafidh Fuadi
2025-03-19 22:57:42 +07:00
parent 0597f0aea0
commit 3b12c7af86
22 changed files with 1886 additions and 1146 deletions

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  NJ<EFBFBD><EFBFBD><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>

}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~
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  <EFBFBD><EFBFBD><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  NJ<EFBFBD><EFBFBD><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  <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\arm64-v8a\android_gradle_build_mini.json  NJ<EFBFBD><EFBFBD><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  <EFBFBD><EFBFBD><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  NJ<EFBFBD><EFBFBD><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  <EFBFBD><EFBFBD><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  NJ<EFBFBD><EFBFBD><EFBFBD>2y
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2y
w
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  NJ<EFBFBD><EFBFBD><EFBFBD>2
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  <EFBFBD><EFBFBD><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  <08><><EFBFBD><EFBFBD><EFBFBD>2 ~
|
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\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\arm64-v8a\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><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  <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><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><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  <08><><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\additional_project_files.txt  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><EFBFBD>2{
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  <08><><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><EFBFBD><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><EFBFBD><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><EFBFBD><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><EFBFBD><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><EFBFBD><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><EFBFBD><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><EFBFBD><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  <08><><EFBFBD><EFBFBD><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~
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><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  <08><><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  <08><><EFBFBD><EFBFBD><EFBFBD>2v
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2v
t
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  <08><><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

@ -23,6 +23,7 @@ class PenerimaPenyaluranModel {
final String? lokasiPenyaluranNama; // Nama lokasi penyaluran
final String? lokasiPenyaluranAlamat; // Alamat lokasi penyaluran
final String? qrCodeHash; // Hash untuk QR code
final String? statusPenyaluran; // Status penyaluran
PenerimaPenyaluranModel({
this.id,
@ -47,6 +48,7 @@ class PenerimaPenyaluranModel {
this.lokasiPenyaluranNama,
this.lokasiPenyaluranAlamat,
this.qrCodeHash,
this.statusPenyaluran,
});
factory PenerimaPenyaluranModel.fromRawJson(String str) =>
@ -82,6 +84,7 @@ class PenerimaPenyaluranModel {
lokasiPenyaluranNama: json["lokasi_penyaluran_nama"],
lokasiPenyaluranAlamat: json["lokasi_penyaluran_alamat"],
qrCodeHash: json["qr_code_hash"],
statusPenyaluran: json["status_penyaluran"],
);
Map<String, dynamic> toJson() => {
@ -107,5 +110,6 @@ class PenerimaPenyaluranModel {
"lokasi_penyaluran_nama": lokasiPenyaluranNama,
"lokasi_penyaluran_alamat": lokasiPenyaluranAlamat,
"qr_code_hash": qrCodeHash,
"status_penyaluran": statusPenyaluran,
};
}

View File

@ -11,6 +11,7 @@ class SkemaBantuanModel {
final DateTime? updatedAt;
final String? stokBantuanId;
final String? kategoriBantuanId;
final double? jumlahDiterimaPerOrang;
SkemaBantuanModel({
this.id,
@ -23,6 +24,7 @@ class SkemaBantuanModel {
this.updatedAt,
this.stokBantuanId,
this.kategoriBantuanId,
this.jumlahDiterimaPerOrang,
});
factory SkemaBantuanModel.fromRawJson(String str) =>
@ -46,6 +48,9 @@ class SkemaBantuanModel {
: null,
stokBantuanId: json["stok_bantuan_id"],
kategoriBantuanId: json["kategori_bantuan_id"],
jumlahDiterimaPerOrang: json["jumlah_diterima_per_orang"] != null
? json["jumlah_diterima_per_orang"].toDouble()
: null,
);
Map<String, dynamic> toJson() => {
@ -59,5 +64,6 @@ class SkemaBantuanModel {
"updated_at": updatedAt?.toIso8601String(),
"stok_bantuan_id": stokBantuanId,
"kategori_bantuan_id": kategoriBantuanId,
"jumlah_diterima_per_orang": jumlahDiterimaPerOrang,
};
}

View File

@ -6,7 +6,6 @@ class TindakanPengaduanModel {
final String? tindakan;
final String? catatan;
final String? statusTindakan; // PROSES, SELESAI
final String? prioritas; // RENDAH, SEDANG, TINGGI
final String? kategoriTindakan; // Kategori tindakan enum
final String? petugasId;
final String? verifikatorId;
@ -27,7 +26,6 @@ class TindakanPengaduanModel {
this.tindakan,
this.catatan,
this.statusTindakan,
this.prioritas,
this.kategoriTindakan,
this.petugasId,
this.verifikatorId,
@ -55,7 +53,6 @@ class TindakanPengaduanModel {
tindakan: json["tindakan"],
catatan: json["catatan"],
statusTindakan: json["status_tindakan"],
prioritas: json["prioritas"],
kategoriTindakan: json["kategori_tindakan"],
petugasId: json["petugas_id"],
verifikatorId: json["verifikator_id"],
@ -76,9 +73,7 @@ class TindakanPengaduanModel {
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
biayaTindakan: json["biaya_tindakan"] != null
? double.parse(json["biaya_tindakan"].toString())
: null,
biayaTindakan: json["biaya_tindakan"]?.toDouble(),
petugas: json["petugas"],
verifikator: json["verifikator"],
);
@ -89,7 +84,6 @@ class TindakanPengaduanModel {
"tindakan": tindakan,
"catatan": catatan,
"status_tindakan": statusTindakan,
"prioritas": prioritas,
"kategori_tindakan": kategoriTindakan,
"petugas_id": petugasId,
"verifikator_id": verifikatorId,
@ -137,20 +131,6 @@ class TindakanPengaduanModel {
}
}
// Getter untuk mendapatkan prioritas yang lebih user-friendly
String get prioritasText {
switch (prioritas) {
case 'RENDAH':
return 'Prioritas Rendah';
case 'SEDANG':
return 'Prioritas Sedang';
case 'TINGGI':
return 'Prioritas Tinggi';
default:
return prioritas ?? 'Tidak Diketahui';
}
}
// Getter untuk mendapatkan kategori tindakan yang lebih user-friendly
String get kategoriTindakanText {
if (kategoriTindakan == null) return 'Tidak Diketahui';

View File

@ -383,6 +383,9 @@ class JadwalPenyaluranController extends GetxController {
required String lokasiPenyaluranId,
required int jumlahPenerima,
required DateTime? tanggalPenyaluran,
required double jumlahDiterimaPerOrang,
required String stokBantuanId,
required double totalStokDibutuhkan,
}) async {
isLoading.value = true;
try {
@ -427,6 +430,7 @@ class JadwalPenyaluranController extends GetxController {
'stok_bantuan_id': skemaBantuanCache[skemaId]?.stokBantuanId,
'status_penerimaan': 'BELUMMENERIMA',
'qr_code_hash': qrCodeHash,
'jumlah_bantuan': jumlahDiterimaPerOrang,
};
// Simpan data penerima ke database
@ -435,6 +439,29 @@ class JadwalPenyaluranController extends GetxController {
.insert(penerimaPenyaluran);
}
// Update stok bantuan (kurangi dengan total stok yang dibutuhkan)
try {
// Dapatkan stok saat ini
final stokData = await _supabaseService.client
.from('stok_bantuan')
.select('total_stok')
.eq('id', stokBantuanId)
.single();
if (stokData != null && stokData['total_stok'] != null) {
final currentStok = stokData['total_stok'].toDouble();
final newStok = currentStok - totalStokDibutuhkan;
// Update stok bantuan dengan nilai baru
await _supabaseService.client
.from('stok_bantuan')
.update({'total_stok': newStok}).eq('id', stokBantuanId);
}
} catch (e) {
print('Error updating stok bantuan: $e');
// Tidak throw exception di sini karena penyaluran sudah disimpan
}
// Setelah berhasil menambahkan, refresh data
await loadJadwalData();
await loadPermintaanPenjadwalanData();

View File

@ -108,7 +108,6 @@ class PengaduanController extends GetxController {
required String tindakan,
required String kategoriTindakan,
required String statusTindakan,
required String prioritas,
String? catatan,
String? hasilTindakan,
required List<String> buktiTindakanPaths,
@ -134,7 +133,6 @@ class PengaduanController extends GetxController {
'tindakan': tindakan,
'catatan': catatan,
'status_tindakan': statusTindakan,
'prioritas': prioritas,
'kategori_tindakan': kategoriTindakan,
'hasil_tindakan': hasilTindakan,
'tanggal_tindakan': DateTime.now().toIso8601String(),
@ -188,7 +186,6 @@ class PengaduanController extends GetxController {
required String tindakan,
required String kategoriTindakan,
required String statusTindakan,
required String prioritas,
String? catatan,
String? hasilTindakan,
required List<String> buktiTindakanPaths,
@ -217,7 +214,6 @@ class PengaduanController extends GetxController {
'tindakan': tindakan,
'catatan': catatan,
'status_tindakan': statusTindakan,
'prioritas': prioritas,
'kategori_tindakan': kategoriTindakan,
'hasil_tindakan': hasilTindakan,
'bukti_tindakan': buktiTindakanUrls,

View File

@ -460,6 +460,92 @@ class DetailPengaduanView extends GetView<PengaduanController> {
),
],
),
const SizedBox(height: 12),
if (pengaduan.fotoPengaduan != null &&
pengaduan.fotoPengaduan!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Foto Pengaduan:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: pengaduan.fotoPengaduan!.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// Tampilkan gambar dalam ukuran penuh saat diklik
Get.to(() => Scaffold(
appBar: AppBar(
title: const Text('Foto Pengaduan'),
backgroundColor: Colors.black,
),
body: Center(
child: InteractiveViewer(
child: Image.network(
pengaduan.fotoPengaduan![index],
fit: BoxFit.contain,
),
),
),
backgroundColor: Colors.black,
));
},
child: Container(
width: 120,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
pengaduan.fotoPengaduan![index],
fit: BoxFit.cover,
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value:
loadingProgress.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey.shade200,
child: const Center(
child:
Icon(Icons.error, color: Colors.red),
),
);
},
),
),
),
);
},
),
),
],
),
const SizedBox(height: 16),
// Panel status pengaduan
_buildStatusPanel(context, pengaduan),
@ -1234,17 +1320,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
),
],
),
// Prioritas tindakan (jika ada)
if (tindakan.prioritas != null) ...[
const SizedBox(height: 8),
// Menggunakan StatusPill untuk prioritas tindakan
StatusPill(
status: tindakan.prioritasText,
backgroundColor: _getPriorityColor(tindakan.prioritas),
textColor: Colors.white,
),
],
// Tampilkan tombol edit jika status PROSES
if (tindakan.statusTindakan == 'PROSES') ...[
const SizedBox(height: 8),
@ -1288,24 +1363,10 @@ class DetailPengaduanView extends GetView<PengaduanController> {
);
}
Color _getPriorityColor(String? priority) {
switch (priority) {
case 'TINGGI':
return Colors.red;
case 'SEDANG':
return Colors.orange;
case 'RENDAH':
return Colors.green;
default:
return Colors.grey;
}
}
void _showTambahTindakanDialog(BuildContext context, String pengaduanId) {
final formKey = GlobalKey<FormState>();
final tindakanController = TextEditingController();
String? selectedKategori;
String? selectedPrioritas;
String selectedStatus = 'PROSES';
final List<String> kategoriOptions = [
@ -1326,12 +1387,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
'PELAPORAN_KE_PIHAK_BERWENANG',
];
final List<String> prioritasOptions = [
'RENDAH',
'SEDANG',
'TINGGI',
];
// Konversi ke format DropdownItem
final List<DropdownItem<String>> kategoriItems = kategoriOptions
.map((kategori) => DropdownItem<String>(
@ -1340,15 +1395,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
))
.toList();
// Konversi ke format DropdownItem untuk prioritas
final List<DropdownItem<String>> prioritasItems = prioritasOptions
.map((prioritas) => DropdownItem<String>(
value: prioritas,
label: prioritas[0].toUpperCase() +
prioritas.substring(1).toLowerCase(),
))
.toList();
showDialog(
context: context,
builder: (context) => AlertDialog(
@ -1380,26 +1426,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
const SizedBox(height: 16),
// Prioritas menggunakan DropdownInput
DropdownInput<String>(
label: 'Prioritas',
hint: 'Pilih prioritas tindakan',
items: prioritasItems,
value: selectedPrioritas,
onChanged: (value) {
selectedPrioritas = value;
},
required: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Pilih prioritas tindakan';
}
return null;
},
),
const SizedBox(height: 16),
// Deskripsi tindakan menggunakan TextInput
TextInput(
label: 'Deskripsi Tindakan',
@ -1433,7 +1459,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
tindakan: tindakanController.text,
kategoriTindakan: selectedKategori ?? '',
statusTindakan: selectedStatus,
prioritas: selectedPrioritas ?? '',
catatan: null,
hasilTindakan: null,
buktiTindakanPaths: [],
@ -1466,7 +1491,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
final hasilTindakanController =
TextEditingController(text: tindakan.hasilTindakan);
String? selectedKategori = tindakan.kategoriTindakan;
String? selectedPrioritas = tindakan.prioritas;
String selectedStatus = 'SELESAI';
// Gunakan List untuk bukti tindakan paths
@ -1865,8 +1889,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
kategoriTindakan: selectedKategori ??
'', // Gunakan kategori yang sudah ada
statusTindakan: selectedStatus,
prioritas: selectedPrioritas ??
'', // Gunakan prioritas yang sudah ada
catatan: catatanController.text.isEmpty
? null
: catatanController.text,

View File

@ -37,7 +37,7 @@ class DetailPenyaluranPage extends StatelessWidget {
title: const Text('Detail Penyaluran'),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
icon: const Icon(Icons.arrow_back, color: AppTheme.primaryColor),
onPressed: () => Get.back(),
),
),
@ -969,8 +969,14 @@ class DetailPenyaluranPage extends StatelessWidget {
backgroundColor: AppTheme.successColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
// Tombol disabled jika belum semua penerima menerima bantuan
disabledBackgroundColor: Colors.grey.shade300,
disabledForegroundColor: Colors.grey.shade700,
),
onPressed: controller.selesaikanPenyaluran,
onPressed: controller.penerimaPenyaluran.every((penerima) =>
penerima.statusPenerimaan?.toUpperCase() == 'DITERIMA')
? controller.selesaikanPenyaluran
: null,
),
),
const SizedBox(width: 12),

View File

@ -321,8 +321,6 @@ class PengaduanView extends GetView<PengaduanController> {
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
} else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt);
}
return Container(
@ -452,7 +450,7 @@ class PengaduanView extends GetView<PengaduanController> {
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal',
label: 'Tanggal Pengaduan',
value: formattedDate,
),
),
@ -631,7 +629,6 @@ class PengaduanView extends GetView<PengaduanController> {
tindakan: controller.tindakanController.text,
kategoriTindakan: 'VERIFIKASI_DATA',
statusTindakan: 'PROSES',
prioritas: 'SEDANG',
catatan: controller.catatanController.text.isEmpty
? null
: controller.catatanController.text,

View File

@ -1,657 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class PengaduanView extends GetView<PengaduanController> {
const PengaduanView({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan pengaduan
_buildPengaduanSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
// Informasi terakhir update
_buildLastUpdateInfo(context),
const SizedBox(height: 20),
// Daftar pengaduan
_buildPengaduanList(context),
],
),
),
);
}
// Tambahkan widget untuk menampilkan waktu terakhir update
Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() {
final lastUpdate = DateTime
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(Icons.update, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Data terupdate: $formattedDate',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
);
});
}
Widget _buildPengaduanSummary(BuildContext context) {
return Obx(() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.pending_actions,
title: 'Diproses',
value: controller.jumlahDiproses.toString(),
color: Colors.orange,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.engineering,
title: 'Tindakan',
value: controller.jumlahTindakan.toString(),
color: Colors.blue,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.check_circle,
title: 'Selesai',
value: controller.jumlahSelesai.toString(),
color: Colors.green,
),
),
],
),
],
),
);
});
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required Color color,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari pengaduan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
onChanged: (value) {
// Implementasi pencarian
controller.refreshData();
},
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: PopupMenuButton<int>(
icon: const Icon(Icons.filter_list),
tooltip: 'Filter',
onSelected: (index) {
controller.changeCategory(index);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 0,
child: Text('Semua'),
),
const PopupMenuItem(
value: 1,
child: Text('Diproses'),
),
const PopupMenuItem(
value: 2,
child: Text('Tindakan'),
),
const PopupMenuItem(
value: 3,
child: Text('Selesai'),
),
],
),
),
],
);
}
Widget _buildPengaduanList(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
final filteredPengaduan = controller.getFilteredPengaduan();
if (filteredPengaduan.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Icon(
Icons.inbox_outlined,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum ada pengaduan',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Pengaduan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => controller.refreshData(),
tooltip: 'Refresh',
),
],
),
],
),
const SizedBox(height: 12),
...filteredPengaduan
.map((item) => _buildPengaduanItem(context, item)),
],
);
});
}
Widget _buildPengaduanItem(BuildContext context, dynamic item) {
Color statusColor;
IconData statusIcon;
switch (item.status?.toUpperCase()) {
case 'MENUNGGU':
statusColor = AppTheme.warningColor;
statusIcon = Icons.pending;
break;
case 'TINDAKAN':
statusColor = AppTheme.infoColor;
statusIcon = Icons.sync;
break;
case 'SELESAI':
statusColor = AppTheme.successColor;
statusIcon = Icons.check_circle;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help_outline;
}
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
} else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt);
}
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.warga?['nama'] ?? item.judul ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 16,
color: statusColor,
),
const SizedBox(width: 4),
Text(
item.status ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 4),
_buildItemDetail(
context,
icon: Icons.person,
label: 'NIK',
value: item.warga?['nik'] ?? '',
),
const SizedBox(height: 12),
if (item.penerimaPenyaluran != null) ...[
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.category,
label: 'Penyaluran',
value: item.namaPenyaluran ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Jenis',
value: item.jenisBantuan ?? '',
),
),
],
),
const SizedBox(height: 8),
_buildItemDetail(
context,
icon: Icons.shopping_bag,
label: 'Jumlah',
value: item.jumlahBantuan ?? '',
),
],
const SizedBox(height: 8),
Text(
item.deskripsi ?? '',
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
_buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal',
value: formattedDate,
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _buildActionButtons(context, item),
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
List<Widget> _buildActionButtons(BuildContext context, dynamic item) {
final status = item.status?.toUpperCase();
if (status == 'MENUNGGU') {
return [
TextButton.icon(
onPressed: () {
// Implementasi untuk memproses pengaduan
_showTindakanDialog(context, item);
},
icon: const Icon(Icons.engineering, size: 18),
label: const Text('Tindakan'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} else if (status == 'TINDAKAN') {
return [
TextButton.icon(
onPressed: () {
// Implementasi untuk menyelesaikan pengaduan
_showSelesaikanDialog(context, item);
},
icon: const Icon(Icons.check_circle, size: 18),
label: const Text('Selesaikan'),
style: TextButton.styleFrom(
foregroundColor: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
} else {
return [
TextButton.icon(
onPressed: () {
// Navigasi ke halaman detail pengaduan
Get.toNamed('/detail-pengaduan', arguments: {'id': item.id});
},
icon: const Icon(Icons.info_outline, size: 18),
label: const Text('Detail'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
];
}
}
void _showTindakanDialog(BuildContext context, dynamic item) {
controller.tindakanController.clear();
controller.catatanController.clear();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tindakan Pengaduan'),
content: Form(
key: controller.tindakanFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'),
const SizedBox(height: 16),
TextFormField(
controller: controller.tindakanController,
decoration: const InputDecoration(
labelText: 'Tindakan yang dilakukan',
border: OutlineInputBorder(),
),
maxLines: 3,
validator: controller.validateTindakan,
),
const SizedBox(height: 12),
TextFormField(
controller: controller.catatanController,
decoration: const InputDecoration(
labelText: 'Catatan (opsional)',
border: OutlineInputBorder(),
),
maxLines: 2,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (controller.tindakanFormKey.currentState!.validate()) {
Navigator.pop(context);
controller.tambahTindakanPengaduan(
pengaduanId: item.id!,
tindakan: controller.tindakanController.text,
kategoriTindakan: 'VERIFIKASI_DATA',
statusTindakan: 'PROSES',
prioritas: 'SEDANG',
catatan: controller.catatanController.text.isEmpty
? null
: controller.catatanController.text,
buktiTindakanPaths: [],
);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showSelesaikanDialog(BuildContext context, dynamic item) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Selesaikan Pengaduan'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Pengaduan dari: ${item.warga?['nama'] ?? ''}'),
const SizedBox(height: 16),
const Text(
'Apakah Anda yakin ingin menyelesaikan pengaduan ini?',
textAlign: TextAlign.center,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
controller.selesaikanPengaduan(item.id!);
},
child: const Text('Selesaikan'),
),
],
),
);
}
}

View File

@ -34,6 +34,13 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
final Rx<SkemaBantuanModel?> selectedSkemaBantuan =
Rx<SkemaBantuanModel?>(null);
final RxInt jumlahPenerima = 0.obs;
final RxDouble jumlahDiterimaPerOrang = 0.0.obs;
final RxString namaStokBantuan = ''.obs;
final RxString satuanStokBantuan = ''.obs;
final RxDouble totalStokDibutuhkan = 0.0.obs;
final RxDouble totalStokTersedia = 0.0.obs;
final RxBool isStokCukup = false.obs;
final RxBool isUang = false.obs;
// Tanggal dan waktu penyaluran
final Rx<DateTime?> selectedDate = Rx<DateTime?>(null);
@ -50,11 +57,72 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
print('pengajuan $pengajuanData');
jumlahPenerima.value = pengajuanData.length;
// Hitung total stok yang dibutuhkan
totalStokDibutuhkan.value =
jumlahPenerima.value * jumlahDiterimaPerOrang.value;
// Perbarui status kecukupan stok
isStokCukup.value =
totalStokTersedia.value >= totalStokDibutuhkan.value;
} catch (e) {
print('Error loading pengajuan kelayakan: $e');
}
}
// Fungsi untuk memuat informasi stok bantuan
Future<void> loadStokBantuanInfo(String stokBantuanId) async {
try {
if (stokBantuanId.isEmpty) {
namaStokBantuan.value = 'Tidak ada stok terkait';
satuanStokBantuan.value = '';
totalStokTersedia.value = 0;
isStokCukup.value = false;
isUang.value = false;
return;
}
final stokData = await controller.supabaseService.client
.from('stok_bantuan')
.select('*')
.eq('id', stokBantuanId)
.single();
print('stokData $stokData');
if (stokData != null) {
namaStokBantuan.value =
stokData['nama'] ?? 'Nama stok tidak tersedia';
satuanStokBantuan.value = stokData['satuan'] ?? 'Tidak ada satuan';
isUang.value = stokData['is_uang'] ?? false;
// Ambil jumlah stok tersedia
if (stokData['total_stok'] != null) {
totalStokTersedia.value = stokData['total_stok'].toDouble();
} else {
totalStokTersedia.value = 0;
}
// Periksa kecukupan stok
isStokCukup.value =
totalStokTersedia.value >= totalStokDibutuhkan.value;
} else {
namaStokBantuan.value = 'Stok tidak ditemukan';
satuanStokBantuan.value = '';
totalStokTersedia.value = 0;
isStokCukup.value = false;
isUang.value = false;
}
} catch (e) {
print('Error loading stok bantuan: $e');
namaStokBantuan.value = 'Error memuat data stok';
satuanStokBantuan.value = '';
totalStokTersedia.value = 0;
isStokCukup.value = false;
isUang.value = false;
}
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
@ -72,33 +140,6 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
),
const SizedBox(height: 24),
// Nama Penyaluran
Text(
'Nama Penyaluran',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: namaController,
decoration: InputDecoration(
hintText: 'Masukkan nama penyaluran',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama penyaluran tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
// Skema Bantuan
Text(
'Skema Bantuan',
@ -128,6 +169,20 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
if (value != null) {
selectedSkemaBantuan.value =
controller.skemaBantuanCache[value];
// Set jumlah yang diterima per orang
jumlahDiterimaPerOrang.value = selectedSkemaBantuan
.value?.jumlahDiterimaPerOrang ??
0.0;
// Load stok bantuan info
if (selectedSkemaBantuan.value?.stokBantuanId != null) {
await loadStokBantuanInfo(
selectedSkemaBantuan.value!.stokBantuanId!);
} else {
namaStokBantuan.value = 'Tidak ada stok terkait';
}
await loadPengajuanKelayakan(value);
}
},
@ -139,6 +194,405 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
},
)),
const SizedBox(height: 16),
// Jumlah Penerima (Otomatis)
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Jumlah Penerima',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(width: 4),
Tooltip(
message:
'Jumlah penerima dari pengajuan kelayakan yang terverifikasi.',
triggerMode: TooltipTriggerMode.tap,
child: Icon(
Icons.info_outline,
size: 16,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
Obx(() => TextFormField(
readOnly: true,
controller: TextEditingController(
text: jumlahPenerima.value.toString()),
decoration: InputDecoration(
hintText:
'Jumlah penerima akan diambil otomatis',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
)),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
children: [
// Jumlah Diterima Per Orang (dari skema)
Row(
children: [
Text(
'Jumlah Per Penerima',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(width: 4),
Tooltip(
message:
'Jumlah yang akan diterima setiap penerima bantuan.',
triggerMode: TooltipTriggerMode.tap,
child: Icon(
Icons.info_outline,
size: 16,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
Obx(() => TextFormField(
readOnly: true,
controller: TextEditingController(
text:
jumlahDiterimaPerOrang.value.toString()),
decoration: InputDecoration(
hintText:
'Jumlah diterima per orang dari skema bantuan',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixText: satuanStokBantuan.value.isNotEmpty
? satuanStokBantuan.value
: 'satuan',
),
)),
],
),
),
],
),
const SizedBox(height: 8),
Text(
'Daftar Penerima',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Obx(() => OutlinedButton.icon(
onPressed: jumlahPenerima.value > 0
? () async {
final pengajuanData = await controller
.supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*, warga:warga_id(*)')
.eq('skema_bantuan_id',
selectedSkemaBantuanId.value ?? '')
.eq('status', 'TERVERIFIKASI');
Get.dialog(
Dialog(
child: Container(
width:
MediaQuery.of(context).size.width * 0.9,
height:
MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Daftar Penerima Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
columnSpacing: 20,
horizontalMargin: 20,
columns: const [
DataColumn(label: Text('No')),
DataColumn(label: Text('Nama')),
DataColumn(label: Text('NIK')),
DataColumn(
label: Text('Alamat')),
],
rows: pengajuanData
.asMap()
.entries
.map((entry) {
final warga =
entry.value['warga'];
return DataRow(
cells: [
DataCell(Text(
'${entry.key + 1}')),
DataCell(Text(
warga['nama_lengkap'] ??
'-')),
DataCell(Text(
warga['nik'] ?? '-')),
DataCell(Text(
warga['alamat'] ??
'-')),
],
);
}).toList(),
),
),
),
),
],
),
),
),
);
}
: null,
icon: const Icon(Icons.people),
label: const Text('Lihat Daftar'),
style: ButtonStyle(
foregroundColor:
WidgetStateProperty.resolveWith<Color>((states) {
return jumlahPenerima.value <= 0
? Colors.grey
: Theme.of(context).primaryColor;
}),
backgroundColor:
WidgetStateProperty.resolveWith<Color>((states) {
return jumlahPenerima.value <= 0
? Colors.grey.withOpacity(0.1)
: Theme.of(context).primaryColor.withOpacity(0.1);
}),
padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: jumlahPenerima.value <= 0
? Colors.grey.withOpacity(0.5)
: Theme.of(context).primaryColor,
),
),
),
),
)),
const SizedBox(height: 16),
// Informasi Stok Bantuan
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informasi Stok Bantuan',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.amber[800],
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Nama Stok'),
const SizedBox(height: 4),
Obx(() => Text(
namaStokBantuan.value,
style: const TextStyle(
fontWeight: FontWeight.bold),
)),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Satuan Stok'),
const SizedBox(height: 4),
Obx(() => Text(
satuanStokBantuan.value,
style: const TextStyle(
fontWeight: FontWeight.bold),
)),
],
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Total Stok Tersedia'),
const SizedBox(height: 4),
Obx(() => Text(
isUang.value
? 'Rp ${DateTimeHelper.formatNumber(totalStokTersedia.value)}'
: '${totalStokTersedia.value} ${satuanStokBantuan.value}',
style: const TextStyle(
fontWeight: FontWeight.bold),
)),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('Total Stok Dibutuhkan'),
const SizedBox(width: 4),
Tooltip(
message:
'Total stok yang dibutuhkan dihitung dari jumlah penerima × jumlah yang diterima per orang',
triggerMode: TooltipTriggerMode.tap,
child: const Icon(Icons.info_outline,
size: 16),
),
],
),
const SizedBox(height: 4),
Obx(() => Text(
isUang.value
? 'Rp ${DateTimeHelper.formatNumber(totalStokDibutuhkan.value)}'
: '${totalStokDibutuhkan.value} ${satuanStokBantuan.value}',
style: const TextStyle(
fontWeight: FontWeight.bold),
)),
],
),
),
],
),
const SizedBox(height: 12),
Obx(() => selectedSkemaBantuanId.value != null
? Container(
padding: const EdgeInsets.symmetric(
vertical: 6, horizontal: 12),
decoration: BoxDecoration(
color: isStokCukup.value
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isStokCukup.value
? Colors.green
: Colors.red,
),
),
child: Row(
children: [
Icon(
isStokCukup.value
? Icons.check_circle
: Icons.error,
color: isStokCukup.value
? Colors.green
: Colors.red,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
isStokCukup.value
? 'Stok tersedia cukup untuk penyaluran'
: 'Stok tidak cukup untuk penyaluran! Tambah stok terlebih dahulu.',
style: TextStyle(
color: isStokCukup.value
? Colors.green[800]
: Colors.red[800],
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
)
: const SizedBox()),
],
),
),
const SizedBox(height: 16),
// Nama Penyaluran
Text(
'Judul Penyaluran',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: namaController,
decoration: InputDecoration(
hintText: 'Masukkan judul penyaluran',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Judul penyaluran tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
// Lokasi Penyaluran
Text(
@ -176,129 +630,26 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
)),
const SizedBox(height: 16),
// Jumlah Penerima (Otomatis)
Text(
'Jumlah Penerima',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Obx(() => TextFormField(
readOnly: true,
controller: TextEditingController(
text: jumlahPenerima.value.toString()),
decoration: InputDecoration(
hintText: 'Jumlah penerima akan diambil otomatis',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
)),
const SizedBox(height: 8),
Text(
'Info: Jumlah penerima diambil dari data pengajuan kelayakan bantuan yang telah terverifikasi untuk skema bantuan yang dipilih.',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 8),
Obx(() => jumlahPenerima.value > 0
? TextButton.icon(
onPressed: () async {
final pengajuanData = await controller
.supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*, warga:warga_id(*)')
.eq('skema_bantuan_id',
selectedSkemaBantuanId.value ?? '')
.eq('status', 'TERVERIFIKASI');
Get.dialog(
Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Daftar Penerima Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
columnSpacing: 20,
horizontalMargin: 20,
columns: const [
DataColumn(label: Text('No')),
DataColumn(label: Text('Nama')),
DataColumn(label: Text('NIK')),
DataColumn(label: Text('Alamat')),
],
rows: pengajuanData
.asMap()
.entries
.map((entry) {
final warga = entry.value['warga'];
return DataRow(
cells: [
DataCell(
Text('${entry.key + 1}')),
DataCell(Text(
warga['nama_lengkap'] ??
'-')),
DataCell(
Text(warga['nik'] ?? '-')),
DataCell(Text(
warga['alamat'] ?? '-')),
],
);
}).toList(),
),
),
),
),
],
),
),
),
);
},
icon: const Icon(Icons.people),
label: const Text('Lihat Daftar Penerima'),
)
: const SizedBox.shrink()),
const SizedBox(height: 8),
// Tanggal Penyaluran
Row(
children: [
Text(
'Tanggal Penyaluran',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(width: 4),
Tooltip(
message:
'Tanggal pelaksanaan minimal 1 hari sebelum dijadwalkan',
triggerMode: TooltipTriggerMode.tap,
child: Icon(
Icons.info_outline,
size: 16,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
TextFormField(
controller: tanggalPenyaluranController,
@ -337,36 +688,7 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
return null;
},
),
const SizedBox(height: 10),
// Hint info tanggal minimal
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Row(
children: [
const Icon(
Icons.info_outline,
color: Colors.blue,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Tanggal pelaksanaan minimal 1 hari sebelum dijadwalkan',
style: TextStyle(
color: Colors.blue[900],
fontSize: 12,
),
),
),
],
),
),
const SizedBox(height: 16),
// Waktu Mulai
@ -445,6 +767,19 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
child: ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
// Periksa kecukupan stok
if (!isStokCukup.value) {
Get.snackbar(
'Stok Tidak Cukup',
'Stok bantuan tidak mencukupi untuk penyaluran ini. Silakan tambah stok terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
return;
}
// Gabungkan tanggal dan waktu mulai
DateTime? tanggalWaktuMulai;
if (selectedDate.value != null &&
@ -468,7 +803,10 @@ class TambahPenyaluranView extends GetView<JadwalPenyaluranController> {
tanggalPenyaluran: tanggalWaktuMulai,
kategoriBantuanId:
selectedSkemaBantuan.value!.kategoriBantuanId!,
);
jumlahDiterimaPerOrang: jumlahDiterimaPerOrang.value,
stokBantuanId:
selectedSkemaBantuan.value!.stokBantuanId!,
totalStokDibutuhkan: totalStokDibutuhkan.value);
}
},
style: ElevatedButton.styleFrom(

View File

@ -110,33 +110,37 @@ class WargaDashboardController extends GetxController {
),
penyaluran_bantuan:penyaluran_bantuan_id(
*,
lokasi_penyaluran(*)
lokasi_penyaluran(*),
kategori_bantuan(*)
)
''').eq('warga_id', wargaId).order('created_at', ascending: false);
final List<PenerimaPenyaluranModel> penerima = [];
// Loop melalui setiap data penerima
for (var item in response) {
// Pastikan data penerima sesuai dengan tipe data yang diharapkan
Map<String, dynamic> sanitizedPenerimaData =
Map<String, dynamic>.from(item);
// Konversi jumlah_bantuan ke double jika bertipe String
if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
var jumlahBantuan = double.tryParse(
sanitizedPenerimaData['jumlah_bantuan'] as String);
sanitizedPenerimaData['jumlah_bantuan'] = jumlahBantuan;
sanitizedPenerimaData['jumlah_bantuan'] =
double.tryParse(sanitizedPenerimaData['jumlah_bantuan']) ?? 0.0;
}
// Tambahkan informasi apakah bantuan uang atau bukan dan satuan
// Ambil data dari stok bantuan jika tersedia
if (sanitizedPenerimaData['stok_bantuan'] != null) {
// Cek apakah bantuan uang
// Cek apakah bantuan berupa uang atau barang
final isUang =
sanitizedPenerimaData['stok_bantuan']['is_uang'] ?? false;
sanitizedPenerimaData['is_uang'] = isUang;
// Ambil satuan
// Ambil satuan bantuan
final satuan = sanitizedPenerimaData['stok_bantuan']['satuan'] ?? '';
sanitizedPenerimaData['satuan'] = satuan;
// Ambil nama kategori bantuan jika tersedia
// Ambil nama kategori bantuan
if (sanitizedPenerimaData['stok_bantuan']['kategori_bantuan'] !=
null) {
final kategoriNama = sanitizedPenerimaData['stok_bantuan']
@ -146,7 +150,7 @@ class WargaDashboardController extends GetxController {
}
}
// Tambahkan informasi dari penyaluran bantuan
// Ambil data dari penyaluran bantuan jika tersedia
if (sanitizedPenerimaData['penyaluran_bantuan'] != null) {
// Ambil nama penyaluran
final namaPenyaluran =
@ -158,6 +162,11 @@ class WargaDashboardController extends GetxController {
sanitizedPenerimaData['penyaluran_bantuan']['deskripsi'] ?? '';
sanitizedPenerimaData['deskripsi_penyaluran'] = deskripsiPenyaluran;
// Ambil status penyaluran
final statusPenyaluran =
sanitizedPenerimaData['penyaluran_bantuan']['status'] ?? '';
sanitizedPenerimaData['status_penyaluran'] = statusPenyaluran;
// Ambil lokasi penyaluran jika tersedia
if (sanitizedPenerimaData['penyaluran_bantuan']
['lokasi_penyaluran'] !=
@ -172,6 +181,19 @@ class WargaDashboardController extends GetxController {
'';
sanitizedPenerimaData['lokasi_penyaluran_alamat'] = lokasiAlamat;
}
// Ambil kategori bantuan dari relasi langsung jika ada
if (sanitizedPenerimaData['penyaluran_bantuan']['kategori_bantuan'] !=
null) {
final kategoriNama = sanitizedPenerimaData['penyaluran_bantuan']
['kategori_bantuan']['nama'] ??
'';
// Jika belum ada kategori_nama dari stok_bantuan, gunakan dari relasi langsung
if (sanitizedPenerimaData['kategori_nama'] == null ||
sanitizedPenerimaData['kategori_nama'].isEmpty) {
sanitizedPenerimaData['kategori_nama'] = kategoriNama;
}
}
}
var model = PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData);
@ -396,4 +418,74 @@ class WargaDashboardController extends GetxController {
);
}
}
// Metode untuk menambahkan pengaduan baru
Future<bool> addPengaduan({
required String judul,
required String deskripsi,
required String penerimaPenyaluranId,
List<String> fotoPengaduanPaths = const [],
}) async {
try {
isLoading.value = true;
// Cari warga_id berdasarkan user_id
final wargaData = await _supabaseService.getWargaByUserId();
if (wargaData == null) {
throw Exception('Data warga tidak ditemukan');
}
final String wargaId = wargaData['id'];
// Upload foto pengaduan jika ada
List<String> fotoPengaduanUrls = [];
if (fotoPengaduanPaths.isNotEmpty) {
fotoPengaduanUrls = await _supabaseService.uploadMultipleFiles(
fotoPengaduanPaths, 'pengaduan', 'foto_pengaduan') ??
[];
}
// Buat objek pengaduan
final Map<String, dynamic> pengaduanData = {
'judul': judul,
'deskripsi': deskripsi,
'status': 'MENUNGGU',
'warga_id': wargaId,
'penerima_penyaluran_id': penerimaPenyaluranId,
'foto_pengaduan': fotoPengaduanUrls,
'tanggal_pengaduan': DateTime.now().toIso8601String(),
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
};
// Simpan pengaduan ke Supabase
await _supabaseService.client.from('pengaduan').insert(pengaduanData);
// Refresh data pengaduan
await fetchPengaduan();
Get.snackbar(
'Berhasil',
'Pengaduan berhasil dibuat dan akan segera diproses',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
return true;
} catch (e) {
print('Error membuat pengaduan: $e');
Get.snackbar(
'Error',
'Gagal membuat pengaduan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return false;
} finally {
isLoading.value = false;
}
}
}

View File

@ -916,36 +916,12 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
),
],
),
// Prioritas tindakan (jika ada)
if (tindakan.prioritas != null) ...[
const SizedBox(height: 8),
// Menggunakan StatusPill untuk prioritas tindakan
StatusPill(
status: tindakan.prioritasText,
backgroundColor: _getPriorityColor(tindakan.prioritas),
textColor: Colors.white,
),
],
],
),
),
);
}
Color _getPriorityColor(String? priority) {
switch (priority) {
case 'TINGGI':
return Colors.red;
case 'SEDANG':
return Colors.orange;
case 'RENDAH':
return Colors.green;
default:
return Colors.grey;
}
}
void showFullScreenImage(BuildContext context, String imageUrl) {
// Buat controller untuk InteractiveViewer
final TransformationController transformationController =

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/widgets/bantuan_card.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class WargaPenerimaanView extends GetView<WargaDashboardController> {
const WargaPenerimaanView({super.key});
@ -23,15 +24,6 @@ class WargaPenerimaanView extends GetView<WargaDashboardController> {
: _buildPenerimaanList(),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigasi ke halaman riwayat penerimaan
Get.toNamed('/riwayat-penyaluran');
},
backgroundColor: Colors.blue,
tooltip: 'Riwayat Penerimaan',
child: const Icon(Icons.history),
),
);
}
@ -43,13 +35,13 @@ class WargaPenerimaanView extends GetView<WargaDashboardController> {
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.volunteer_activism,
size: 80,
color: Colors.blue.shade400,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 24),
@ -82,6 +74,8 @@ class WargaPenerimaanView extends GetView<WargaDashboardController> {
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),

View File

@ -30,13 +30,13 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.report_problem,
Icons.check_circle_outline,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum Ada Pengaduan',
'Bagus!',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -45,27 +45,12 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
),
const SizedBox(height: 8),
Text(
'Anda belum membuat pengaduan',
'Belum ada pengaduan yang dibuat',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
// TODO: Implementasi navigasi ke halaman buat pengaduan
Get.toNamed('/buat-pengaduan');
},
icon: const Icon(Icons.add),
label: const Text('Buat Pengaduan Baru'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
);

View File

@ -108,16 +108,6 @@ class WargaView extends GetView<WargaDashboardController> {
badgeColor: Colors.orange,
onTap: () => controller.changeTab(2),
),
DrawerMenuItem(
icon: Icons.assignment_outlined,
title: 'Pengajuan Kelayakan',
onTap: () {
// TODO: Navigasi ke halaman pengajuan kelayakan
Get.toNamed('/pengajuan-kelayakan');
},
badgeCount: controller.totalPengajuanMenunggu.value,
badgeColor: Colors.blue,
),
],
)),
body: Obx(() {
@ -161,20 +151,6 @@ class WargaView extends GetView<WargaDashboardController> {
),
],
)),
floatingActionButton: Obx(() {
// Tampilkan FAB hanya di halaman pengaduan
if (controller.activeTabIndex.value == 2) {
return FloatingActionButton(
onPressed: () {
// TODO: Implementasi navigasi ke halaman buat pengaduan
Get.toNamed('/buat-pengaduan');
},
backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.add),
);
}
return const SizedBox.shrink();
}),
);
}
}

View File

@ -112,6 +112,19 @@ class BantuanCard extends StatelessWidget {
vertical: 4,
),
),
if (item.statusPenyaluran != null &&
item.statusPenyaluran!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: _buildPenyaluranStatusBadge(
item.statusPenyaluran!,
fontSize: 10,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
),
),
const SizedBox(height: 8),
Text(
formattedJumlah,
@ -159,11 +172,17 @@ class BantuanCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
Wrap(
spacing: 8,
children: [
StatusBadge(status: item.statusPenyaluran ?? ""),
StatusBadge(
status: item.statusPenerimaan ?? 'MENUNGGU',
),
],
),
],
),
if (item.deskripsiPenyaluran != null &&
item.deskripsiPenyaluran!.isNotEmpty)
Padding(
@ -276,4 +295,51 @@ class BantuanCard extends StatelessWidget {
),
);
}
// Widget untuk menampilkan badge status penyaluran
Widget _buildPenyaluranStatusBadge(
String status, {
double fontSize = 12,
EdgeInsets padding =
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
}) {
Color statusColor;
String statusText;
switch (status.toUpperCase()) {
case 'DIJADWALKAN':
case 'DISETUJUI':
statusColor = Colors.blue;
statusText = 'Dijadwalkan';
break;
case 'TERLAKSANA':
statusColor = Colors.green;
statusText = 'Terlaksana';
break;
case 'BATALTERLAKSANA':
statusColor = Colors.red;
statusText = 'Dibatalkan';
break;
default:
statusColor = Colors.grey;
statusText = status;
}
return Container(
padding: padding,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: statusColor),
),
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
),
);
}
}

View File

@ -30,6 +30,10 @@ class StatusBadge extends StatelessWidget {
'TINDAKAN': Colors.orange,
'SELESAI': Colors.green,
'TERVERIFIKASI': Colors.green,
'BELUMMENERIMA': Colors.orange,
'DIJADWALKAN': Colors.blue,
'TERLAKSANA': Colors.purple,
'AKTIF': Colors.green,
};
// Default labels for common statuses
@ -42,6 +46,10 @@ class StatusBadge extends StatelessWidget {
'TINDAKAN': 'Tindakan',
'SELESAI': 'Selesai',
'TERVERIFIKASI': 'Terverifikasi',
'BELUMMENERIMA': 'Belum Menerima',
'DIJADWALKAN': 'Dijadwalkan',
'TERLAKSANA': 'Terlaksana',
'AKTIF': 'Aktif',
};
// Determine color and label based on status