Perbarui judul aplikasi dari 'Penyaluran App' menjadi 'Penerimaan App'. Tambahkan properti baru pada model PenerimaPenyaluranModel untuk mendukung informasi tambahan terkait penyaluran. Modifikasi tampilan di WargaDashboardView dan WargaPengaduanView untuk meningkatkan pengalaman pengguna. Hapus WargaPenyaluranView yang tidak digunakan dan perbarui rute aplikasi untuk mencerminkan perubahan ini.

This commit is contained in:
Khafidh Fuadi
2025-03-16 19:37:37 +07:00
parent a3798f0005
commit 76b167c65c
19 changed files with 1806 additions and 757 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  <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>

}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  <08><><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  <08><><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><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  <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\arm64-v8a\android_gradle_build_mini.json  <08><><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  <08><><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><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  <08><><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><EFBFBD><EFBFBD>2y
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <08><><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><EFBFBD><EFBFBD>2
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  <08><><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  <EFBFBD><EFBFBD><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  ů<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  <EFBFBD><EFBFBD><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  ů<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  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2s
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  ů<EFBFBD><EFBFBD><EFBFBD>2s
q
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  ů<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  ϯ<><CFAF><EFBFBD>2 x
v
v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  ϯ<><CFAF><EFBFBD>2
~
|
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  ϯ<><CFAF><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  ϯ<><CFAF><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><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  ̱<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><EFBFBD><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  ̱<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><EFBFBD><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  ̱<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><EFBFBD><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  ̱<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><EFBFBD><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  ̱<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><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2v
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  ̱<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><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  ̱<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  ̱<><CCB1><EFBFBD>2 {
y
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  ̱<><CCB1><EFBFBD>2
<EFBFBD>


}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  ̱<><CCB1><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  ̱<><CCB1><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2y
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2y

View File

@ -13,6 +13,15 @@ class PenerimaPenyaluranModel {
final String? stokBantuanId;
final Map<String, dynamic>? warga; // Data warga yang terkait
final String? tandaTangan;
final bool? isUang; // Apakah bantuan berupa uang
final String? satuan; // Satuan bantuan
final Map<String, dynamic>? stokBantuan; // Data stok bantuan
final Map<String, dynamic>? penyaluranBantuan; // Data penyaluran bantuan
final String? kategoriNama; // Nama kategori bantuan
final String? namaPenyaluran; // Nama penyaluran
final String? deskripsiPenyaluran; // Deskripsi penyaluran
final String? lokasiPenyaluranNama; // Nama lokasi penyaluran
final String? lokasiPenyaluranAlamat; // Alamat lokasi penyaluran
PenerimaPenyaluranModel({
this.id,
@ -27,6 +36,15 @@ class PenerimaPenyaluranModel {
this.stokBantuanId,
this.warga,
this.tandaTangan,
this.isUang,
this.satuan,
this.stokBantuan,
this.penyaluranBantuan,
this.kategoriNama,
this.namaPenyaluran,
this.deskripsiPenyaluran,
this.lokasiPenyaluranNama,
this.lokasiPenyaluranAlamat,
});
factory PenerimaPenyaluranModel.fromRawJson(String str) =>
@ -52,6 +70,15 @@ class PenerimaPenyaluranModel {
stokBantuanId: json["stok_bantuan_id"],
warga: json["warga"],
tandaTangan: json["tanda_tangan"],
isUang: json["is_uang"],
satuan: json["satuan"],
stokBantuan: json["stok_bantuan"],
penyaluranBantuan: json["penyaluran_bantuan"],
kategoriNama: json["kategori_nama"],
namaPenyaluran: json["nama_penyaluran"],
deskripsiPenyaluran: json["deskripsi_penyaluran"],
lokasiPenyaluranNama: json["lokasi_penyaluran_nama"],
lokasiPenyaluranAlamat: json["lokasi_penyaluran_alamat"],
);
Map<String, dynamic> toJson() => {
@ -67,5 +94,14 @@ class PenerimaPenyaluranModel {
"stok_bantuan_id": stokBantuanId,
"warga": warga,
"tanda_tangan": tandaTangan,
"is_uang": isUang,
"satuan": satuan,
"stok_bantuan": stokBantuan,
"penyaluran_bantuan": penyaluranBantuan,
"kategori_nama": kategoriNama,
"nama_penyaluran": namaPenyaluran,
"deskripsi_penyaluran": deskripsiPenyaluran,
"lokasi_penyaluran_nama": lokasiPenyaluranNama,
"lokasi_penyaluran_alamat": lokasiPenyaluranAlamat,
};
}

View File

@ -35,7 +35,7 @@ class DetailPenyaluranPage extends StatelessWidget {
title: const Text('Detail Penyaluran'),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Get.back(),
),
),

View File

@ -4,9 +4,11 @@ import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class WargaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final Rx<UserModel?> currentUser = Rx<UserModel?>(null);
@ -58,84 +60,22 @@ class WargaDashboardController extends GetxController {
isLoading.value = true;
try {
// TODO: Implementasi fetch data dari API
// Contoh data dummy untuk pengembangan UI
await Future.delayed(const Duration(seconds: 1));
// Pastikan user sudah login dan memiliki ID
if (user?.id == null) {
throw Exception('User tidak terautentikasi');
}
// Dummy data penerima penyaluran
penerimaPenyaluran.value = [
PenerimaPenyaluranModel(
id: '1',
statusPenerimaan: 'DITERIMA',
tanggalPenerimaan: DateTime.now().subtract(const Duration(days: 5)),
jumlahBantuan: 50000,
keterangan: 'Bantuan Tunai',
),
PenerimaPenyaluranModel(
id: '2',
statusPenerimaan: 'DITERIMA',
tanggalPenerimaan: DateTime.now().subtract(const Duration(days: 15)),
jumlahBantuan: 100000,
keterangan: 'Bantuan Sembako',
),
];
// Ambil data penerima penyaluran dari server
await fetchPenerimaPenyaluran();
totalPenyaluranDiterima.value = penerimaPenyaluran.length;
// Ambil data pengajuan kelayakan dari server
await fetchPengajuanKelayakan();
// Dummy data pengajuan kelayakan
pengajuanKelayakan.value = [
PengajuanKelayakanBantuanModel(
id: '1',
status: StatusKelayakan.MENUNGGU,
createdAt: DateTime.now().subtract(const Duration(days: 2)),
),
PengajuanKelayakanBantuanModel(
id: '2',
status: StatusKelayakan.TERVERIFIKASI,
createdAt: DateTime.now().subtract(const Duration(days: 10)),
),
PengajuanKelayakanBantuanModel(
id: '3',
status: StatusKelayakan.DITOLAK,
createdAt: DateTime.now().subtract(const Duration(days: 20)),
alasanVerifikasi: 'Dokumen tidak lengkap',
),
];
// Ambil data pengaduan dari server
await fetchPengaduan();
totalPengajuanMenunggu.value = pengajuanKelayakan
.where((p) => p.status == StatusKelayakan.MENUNGGU)
.length;
totalPengajuanTerverifikasi.value = pengajuanKelayakan
.where((p) => p.status == StatusKelayakan.TERVERIFIKASI)
.length;
totalPengajuanDitolak.value = pengajuanKelayakan
.where((p) => p.status == StatusKelayakan.DITOLAK)
.length;
// Dummy data pengaduan
pengaduan.value = [
PengaduanModel(
id: '1',
judul: 'Bantuan tidak sesuai',
status: 'PROSES',
tanggalPengaduan: DateTime.now().subtract(const Duration(days: 3)),
),
PengaduanModel(
id: '2',
judul: 'Keterlambatan penyaluran',
status: 'SELESAI',
tanggalPengaduan: DateTime.now().subtract(const Duration(days: 25)),
),
];
totalPengaduan.value = pengaduan.length;
totalPengaduanProses.value =
pengaduan.where((p) => p.status == 'PROSES').length;
totalPengaduanSelesai.value =
pengaduan.where((p) => p.status == 'SELESAI').length;
// Dummy data notifikasi
jumlahNotifikasiBelumDibaca.value = 3;
// Ambil data notifikasi
await fetchNotifikasi();
} catch (e) {
print('Error fetching data: $e');
} finally {
@ -143,6 +83,239 @@ class WargaDashboardController extends GetxController {
}
}
// Fungsi untuk mengambil data penerima penyaluran
Future<void> fetchPenerimaPenyaluran() async {
try {
print('Memulai fetchPenerimaPenyaluran()');
print('User ID: ${user?.id}');
// Pertama, cari warga_id berdasarkan user_id
final wargaResponse = await _supabaseService.client
.from('warga')
.select('id')
.eq('user_id', user!.id)
.single();
print('Warga response: $wargaResponse');
if (wargaResponse == null) {
print('Tidak ditemukan data warga untuk user_id: ${user!.id}');
return;
}
final wargaId = wargaResponse['id'];
print('Warga ID: $wargaId');
// Ambil data penerima penyaluran dengan join ke warga, stok bantuan, dan penyaluran bantuan
final response =
await _supabaseService.client.from('penerima_penyaluran').select('''
*,
warga:warga_id(*),
stok_bantuan:stok_bantuan_id(
*,
kategori_bantuan(*)
),
penyaluran_bantuan:penyaluran_bantuan_id(
*,
lokasi_penyaluran(*)
)
''').eq('warga_id', wargaId).order('created_at', ascending: false);
print('Response dari API: $response');
if (response != null) {
final List<PenerimaPenyaluranModel> penerima = [];
for (var item in response) {
print('Memproses item: $item');
Map<String, dynamic> sanitizedPenerimaData =
Map<String, dynamic>.from(item);
print('Data yang disanitasi: $sanitizedPenerimaData');
if (sanitizedPenerimaData['jumlah_bantuan'] is String) {
var jumlahBantuan = double.tryParse(
sanitizedPenerimaData['jumlah_bantuan'] as String);
print(
'Konversi jumlah_bantuan dari String ke double: $jumlahBantuan');
sanitizedPenerimaData['jumlah_bantuan'] = jumlahBantuan;
}
// Tambahkan informasi apakah bantuan uang atau bukan dan satuan
if (sanitizedPenerimaData['stok_bantuan'] != null) {
print('Stok bantuan: ${sanitizedPenerimaData['stok_bantuan']}');
// Cek apakah bantuan uang
final isUang =
sanitizedPenerimaData['stok_bantuan']['is_uang'] ?? false;
sanitizedPenerimaData['is_uang'] = isUang;
// Ambil satuan
final satuan =
sanitizedPenerimaData['stok_bantuan']['satuan'] ?? '';
sanitizedPenerimaData['satuan'] = satuan;
// Ambil nama kategori bantuan jika tersedia
if (sanitizedPenerimaData['stok_bantuan']['kategori_bantuan'] !=
null) {
final kategoriNama = sanitizedPenerimaData['stok_bantuan']
['kategori_bantuan']['nama'] ??
'';
sanitizedPenerimaData['kategori_nama'] = kategoriNama;
}
print('Is Uang: $isUang, Satuan: $satuan');
}
// Tambahkan informasi dari penyaluran bantuan
if (sanitizedPenerimaData['penyaluran_bantuan'] != null) {
print(
'Penyaluran bantuan: ${sanitizedPenerimaData['penyaluran_bantuan']}');
// Ambil nama penyaluran
final namaPenyaluran =
sanitizedPenerimaData['penyaluran_bantuan']['nama'] ?? '';
sanitizedPenerimaData['nama_penyaluran'] = namaPenyaluran;
// Ambil deskripsi penyaluran
final deskripsiPenyaluran =
sanitizedPenerimaData['penyaluran_bantuan']['deskripsi'] ?? '';
sanitizedPenerimaData['deskripsi_penyaluran'] = deskripsiPenyaluran;
// Ambil lokasi penyaluran jika tersedia
if (sanitizedPenerimaData['penyaluran_bantuan']
['lokasi_penyaluran'] !=
null) {
final lokasiNama = sanitizedPenerimaData['penyaluran_bantuan']
['lokasi_penyaluran']['nama'] ??
'';
sanitizedPenerimaData['lokasi_penyaluran_nama'] = lokasiNama;
final lokasiAlamat = sanitizedPenerimaData['penyaluran_bantuan']
['lokasi_penyaluran']['alamat_lengkap'] ??
'';
sanitizedPenerimaData['lokasi_penyaluran_alamat'] = lokasiAlamat;
}
print('Nama Penyaluran: $namaPenyaluran');
}
var model = PenerimaPenyaluranModel.fromJson(sanitizedPenerimaData);
print('Model yang dibuat: $model');
penerima.add(model);
}
print('Total data yang diproses: ${penerima.length}');
penerimaPenyaluran.assignAll(penerima);
var diterima =
penerima.where((p) => p.statusPenerimaan == 'DITERIMA').length;
print('Total penyaluran diterima: $diterima');
totalPenyaluranDiterima.value = diterima;
}
} catch (e, stackTrace) {
print('Error fetchPenerimaPenyaluran: $e');
print('Stack trace: $stackTrace');
}
}
// Fungsi untuk mengambil data pengajuan kelayakan
Future<void> fetchPengajuanKelayakan() async {
try {
// Pertama, cari warga_id berdasarkan user_id
final wargaResponse = await _supabaseService.client
.from('warga')
.select('id')
.eq('user_id', user!.id)
.single();
if (wargaResponse == null) {
print('Tidak ditemukan data warga untuk user_id: ${user!.id}');
return;
}
final wargaId = wargaResponse['id'];
print('Warga ID untuk pengajuan kelayakan: $wargaId');
final response = await _supabaseService.client
.from('xx02_pengajuan_kelayakan_bantuan')
.select('*')
.eq('warga_id', wargaId)
.order('created_at', ascending: false);
if (response != null) {
final List<PengajuanKelayakanBantuanModel> pengajuan = [];
for (var item in response) {
// Konversi status ke enum
if (item['status'] != null) {
final statusStr = item['status'].toString();
item['status'] = statusStr; // Pastikan status dalam format string
}
pengajuan.add(PengajuanKelayakanBantuanModel.fromJson(item));
}
pengajuanKelayakan.assignAll(pengajuan);
// Hitung jumlah berdasarkan status
totalPengajuanMenunggu.value =
pengajuan.where((p) => p.status == StatusKelayakan.MENUNGGU).length;
totalPengajuanTerverifikasi.value = pengajuan
.where((p) => p.status == StatusKelayakan.TERVERIFIKASI)
.length;
totalPengajuanDitolak.value =
pengajuan.where((p) => p.status == StatusKelayakan.DITOLAK).length;
}
} catch (e) {
print('Error fetching pengajuan kelayakan: $e');
}
}
// Fungsi untuk mengambil data pengaduan
Future<void> fetchPengaduan() async {
try {
final response = await _supabaseService.client
.from('pengaduan')
.select('*')
.eq('pelapor', user!.id)
.order('created_at', ascending: false);
if (response != null) {
final List<PengaduanModel> pengaduanList = [];
for (var item in response) {
pengaduanList.add(PengaduanModel.fromJson(item));
}
pengaduan.assignAll(pengaduanList);
// Hitung jumlah berdasarkan status
totalPengaduan.value = pengaduanList.length;
totalPengaduanProses.value = pengaduanList
.where((p) => p.status == 'PROSES' || p.status == 'DIPROSES')
.length;
totalPengaduanSelesai.value =
pengaduanList.where((p) => p.status == 'SELESAI').length;
}
} catch (e) {
print('Error fetching pengaduan: $e');
}
}
// Fungsi untuk mengambil data notifikasi
Future<void> fetchNotifikasi() async {
try {
final response = await _supabaseService.client
.from('notifikasi')
.select('*')
.eq('user_id', user!.id)
.eq('dibaca', false)
.count();
jumlahNotifikasiBelumDibaca.value = response.count;
} catch (e) {
print('Error fetching notifikasi: $e');
jumlahNotifikasiBelumDibaca.value = 0;
}
}
// Navigasi ke halaman detail
void goToPenyaluranDetail() {
changeTab(1);

View File

@ -2,91 +2,147 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/bantuan_card.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
class WargaDashboardView extends GetView<WargaDashboardController> {
const WargaDashboardView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildPenyaluranSummary(),
const SizedBox(height: 24),
_buildPengajuanSection(),
const SizedBox(height: 24),
_buildPengaduanSummary(),
],
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildPenerimaanSummary(),
const SizedBox(height: 24),
_buildRecentPenerimaan(),
],
),
),
),
);
});
);
}),
);
}
Widget _buildWelcomeSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.blue.shade100,
child: const Icon(
Icons.person,
size: 40,
color: Colors.blue,
),
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.shade100,
child: Icon(
Icons.person,
color: Colors.blue.shade700,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang,',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
controller.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang,',
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.home,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Alamat tidak tersedia',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
color: Colors.grey.shade700,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
controller.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.phone,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
'No. HP tidak tersedia',
style: TextStyle(
color: Colors.grey.shade700,
),
const SizedBox(height: 4),
Text(
controller.desa ?? 'Warga Desa',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.location_city,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
controller.desa ?? 'Desa tidak tersedia',
style: TextStyle(
color: Colors.grey.shade700,
),
],
),
),
],
),
],
),
@ -94,393 +150,179 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
);
}
Widget _buildPenyaluranSummary() {
Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Penyaluran Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Bantuan Diterima',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'${controller.totalPenyaluranDiterima.value}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const Icon(
Icons.volunteer_activism,
size: 40,
color: Colors.blue,
),
],
),
const Divider(height: 32),
if (controller.penerimaPenyaluran.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
item.keterangan ?? 'Bantuan',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPenerimaan!)
: '-',
),
trailing: Text(
item.jumlahBantuan != null
? currencyFormat.format(item.jumlahBantuan)
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada penyaluran bantuan'),
),
),
if (controller.penerimaPenyaluran.length > 2)
TextButton(
onPressed: () => controller.changeTab(1),
child: const Text('Lihat Semua'),
),
],
),
),
),
],
);
}
// Hitung total bantuan uang dan non-uang
double totalUang = 0;
Map<String, double> totalNonUang = {};
Widget _buildPengajuanSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengajuan Kelayakan Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatusCounter(
'Menunggu',
controller.totalPengajuanMenunggu.value,
Colors.orange,
),
_buildStatusCounter(
'Terverifikasi',
controller.totalPengajuanTerverifikasi.value,
Colors.green,
),
_buildStatusCounter(
'Ditolak',
controller.totalPengajuanDitolak.value,
Colors.red,
),
],
),
const Divider(height: 32),
if (controller.pengajuanKelayakan.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.pengajuanKelayakan.length > 3
? 3
: controller.pengajuanKelayakan.length,
itemBuilder: (context, index) {
final item = controller.pengajuanKelayakan[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Pengajuan #${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.createdAt != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.createdAt!)
: '-',
),
trailing: _buildStatusBadge(item.status),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada pengajuan kelayakan'),
),
),
if (controller.pengajuanKelayakan.length > 3)
TextButton(
onPressed: controller.goToPengajuanDetail,
child: const Text('Lihat Semua'),
),
],
),
),
),
],
);
}
Widget _buildStatusCounter(String label, int count, Color color) {
return Column(
children: [
Text(
'$count',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
);
}
Widget _buildStatusBadge(StatusKelayakan? status) {
if (status == null) return const SizedBox();
Color color;
String text;
switch (status) {
case StatusKelayakan.MENUNGGU:
color = Colors.orange;
text = 'Menunggu';
break;
case StatusKelayakan.TERVERIFIKASI:
color = Colors.green;
text = 'Terverifikasi';
break;
case StatusKelayakan.DITOLAK:
color = Colors.red;
text = 'Ditolak';
break;
for (var item in controller.penerimaPenyaluran) {
if (item.statusPenerimaan == 'DITERIMA') {
if (item.isUang == true && item.jumlahBantuan != null) {
totalUang += item.jumlahBantuan!;
} else if (item.isUang == false &&
item.jumlahBantuan != null &&
item.satuan != null) {
if (totalNonUang.containsKey(item.satuan)) {
totalNonUang[item.satuan!] =
(totalNonUang[item.satuan] ?? 0) + item.jumlahBantuan!;
} else {
totalNonUang[item.satuan!] = item.jumlahBantuan!;
}
}
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color),
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Ringkasan Bantuan',
titleStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (totalUang > 0)
_buildSummaryItem(
icon: Icons.attach_money,
color: Colors.green,
title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang),
),
if (totalNonUang.isNotEmpty) ...[
if (totalUang > 0) const SizedBox(height: 12),
...totalNonUang.entries.map((entry) {
return _buildSummaryItem(
icon: Icons.inventory_2,
color: Colors.blue,
title: 'Total Bantuan ${entry.key}',
value: '${entry.value} ${entry.key}',
);
}),
],
if (totalUang == 0 && totalNonUang.isEmpty)
_buildSummaryItem(
icon: Icons.info_outline,
color: Colors.grey,
title: 'Belum Ada Bantuan',
value: 'Anda belum menerima bantuan',
),
],
),
),
);
}
Widget _buildPengaduanSummary() {
Widget _buildSummaryItem({
required IconData icon,
required Color color,
required String title,
required String value,
}) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
),
],
);
}
Widget _buildRecentPenerimaan() {
if (controller.penerimaPenyaluran.isEmpty) {
return const SizedBox.shrink();
}
final maxItems = controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengaduan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed('/warga-penerimaan');
},
),
const SizedBox(height: 12),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatusCounter(
'Total',
controller.totalPengaduan.value,
Colors.blue,
),
_buildStatusCounter(
'Proses',
controller.totalPengaduanProses.value,
Colors.orange,
),
_buildStatusCounter(
'Selesai',
controller.totalPengaduanSelesai.value,
Colors.green,
),
],
),
const Divider(height: 32),
if (controller.pengaduan.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.pengaduan.length > 2
? 2
: controller.pengaduan.length,
itemBuilder: (context, index) {
final item = controller.pengaduan[index];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
item.judul ?? 'Pengaduan #${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
item.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPengaduan!)
: '-',
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: item.status == 'PROSES'
? Colors.orange.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: item.status == 'PROSES'
? Colors.orange
: Colors.green,
),
),
child: Text(
item.status == 'PROSES' ? 'Proses' : 'Selesai',
style: TextStyle(
color: item.status == 'PROSES'
? Colors.orange
: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
);
},
)
else
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text('Belum ada pengaduan'),
),
),
if (controller.pengaduan.length > 2)
TextButton(
onPressed: () => controller.changeTab(2),
child: const Text('Lihat Semua'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
// TODO: Implementasi navigasi ke halaman buat pengaduan
},
icon: const Icon(Icons.add),
label: const Text('Buat Pengaduan Baru'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
),
],
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: maxItems,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: BantuanCard(
item: item,
isCompact: true,
onTap: () {
Get.toNamed('/warga/detail-penerimaan',
arguments: {'id': item.id});
},
),
);
},
),
if (controller.penerimaPenyaluran.length > 2)
Center(
child: TextButton.icon(
onPressed: () {
Get.toNamed('/warga-penerimaan');
},
icon: const Icon(Icons.list),
label: const Text('Lihat Semua Bantuan'),
),
),
),
],
);
}

View File

@ -0,0 +1,632 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';
class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
const WargaDetailPenerimaanView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final Map<String, dynamic> args = Get.arguments ?? {};
final id = args['id'];
if (id == null) {
return Scaffold(
appBar: AppBar(
title: const Text('Detail Penerimaan'),
),
body: const Center(
child: Text('ID Penerimaan tidak ditemukan'),
),
);
}
// 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);
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';
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) {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
String formattedJumlah = '';
if (penyaluran.jumlahBantuan != null) {
if (penyaluran.isUang == true) {
formattedJumlah = currencyFormat.format(penyaluran.jumlahBantuan);
} else {
formattedJumlah =
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
}
} else {
formattedJumlah = '-';
}
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: AppTheme.primaryGradient,
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
penyaluran.namaPenyaluran ??
penyaluran.keterangan ??
'Bantuan',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
StatusBadge(
status: penyaluran.statusPenerimaan ?? 'MENUNGGU',
fontSize: 14,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
),
],
),
const SizedBox(height: 16),
if (penyaluran.deskripsiPenyaluran != null &&
penyaluran.deskripsiPenyaluran!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
penyaluran.deskripsiPenyaluran!,
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: (penyaluran.isUang == true
? Colors.green
: Colors.blue)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
penyaluran.isUang == true
? Icons.attach_money
: Icons.inventory_2,
color: penyaluran.isUang == true
? Colors.green
: Colors.blue,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jumlah Bantuan',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
formattedJumlah,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: penyaluran.isUang == true
? Colors.green.shade700
: Colors.blue.shade700,
),
),
],
),
),
],
),
),
],
),
),
);
}
Widget _buildDetailSection(PenerimaPenyaluranModel penyaluran) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Get.theme.primaryColor,
),
const SizedBox(width: 8),
const Text(
'Detail Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_buildDetailItem(
icon: Icons.category,
title: 'Kategori',
value: penyaluran.kategoriNama ?? 'Tidak tersedia',
),
const Divider(height: 24),
_buildDetailItem(
icon: Icons.calendar_today,
title: 'Tanggal Penerimaan',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
: 'Belum diterima',
),
const Divider(height: 24),
_buildDetailItem(
icon: Icons.access_time,
title: 'Waktu Penerimaan',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('HH:mm', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
: 'Belum diterima',
),
if (penyaluran.keterangan != null &&
penyaluran.keterangan!.isNotEmpty) ...[
const Divider(height: 24),
_buildDetailItem(
icon: Icons.note,
title: 'Keterangan',
value: penyaluran.keterangan!,
),
],
],
),
),
);
}
Widget _buildLocationSection(PenerimaPenyaluranModel penyaluran) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.location_on,
color: Get.theme.primaryColor,
),
const SizedBox(width: 8),
const Text(
'Lokasi Penerimaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_buildDetailItem(
icon: Icons.place,
title: 'Tempat Penerimaan',
value: penyaluran.lokasiPenyaluranNama ?? 'Tidak tersedia',
),
if (penyaluran.lokasiPenyaluranAlamat != null &&
penyaluran.lokasiPenyaluranAlamat!.isNotEmpty) ...[
const Divider(height: 24),
_buildDetailItem(
icon: Icons.map,
title: 'Alamat',
value: penyaluran.lokasiPenyaluranAlamat!,
),
],
const SizedBox(height: 16),
// TODO: Implementasi peta lokasi jika koordinat tersedia
],
),
),
);
}
Widget _buildBuktiPenerimaanSection(PenerimaPenyaluranModel penyaluran) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.verified,
color: Colors.green.shade700,
),
const SizedBox(width: 8),
const Text(
'Bukti Penerimaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
// Bukti Penerimaan (Foto)
if (penyaluran.buktiPenerimaan != null &&
penyaluran.buktiPenerimaan!.isNotEmpty) ...[
const Text(
'Foto Bukti Penerimaan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
penyaluran.buktiPenerimaan!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broken_image,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Gambar tidak dapat dimuat',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
);
},
),
),
),
const SizedBox(height: 16),
] else ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.image_not_supported,
color: Colors.grey.shade600,
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Bukti penerimaan belum diunggah',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
),
],
),
),
const SizedBox(height: 16),
],
// Tanda Tangan
const Divider(height: 24),
const Text(
'Tanda Tangan Penerima',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
if (penyaluran.tandaTangan != null &&
penyaluran.tandaTangan!.isNotEmpty) ...[
Container(
height: 150,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
color: Colors.white,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
penyaluran.tandaTangan!,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broken_image,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 8),
Text(
'Tanda tangan tidak dapat dimuat',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
);
},
),
),
),
] else ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.draw,
color: Colors.grey.shade600,
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Tanda tangan belum tersedia',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
),
],
),
),
],
],
),
),
);
}
Widget _buildAdditionalInfoSection(PenerimaPenyaluranModel penyaluran) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.more_horiz,
color: Get.theme.primaryColor,
),
const SizedBox(width: 8),
const Text(
'Informasi Tambahan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_buildDetailItem(
icon: Icons.numbers,
title: 'ID Penerimaan',
value: '${penyaluran.id}',
),
const Divider(height: 24),
_buildDetailItem(
icon: Icons.person,
title: 'Penerima',
value: controller.nama,
),
const Divider(height: 24),
_buildDetailItem(
icon: Icons.update,
title: 'Terakhir Diperbarui',
value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
.format(penyaluran.tanggalPenerimaan!)
: 'Tidak tersedia',
),
],
),
),
);
}
Widget _buildDetailItem({
required IconData icon,
required String title,
required String value,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 20,
color: Colors.grey.shade700,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
}

View File

@ -0,0 +1,112 @@
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';
class WargaPenerimaanView extends GetView<WargaDashboardController> {
const WargaPenerimaanView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: controller.penerimaPenyaluran.isEmpty
? _buildEmptyState()
: _buildPenerimaanList(),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigasi ke halaman riwayat penerimaan
Get.toNamed('/riwayat-penyaluran');
},
backgroundColor: Colors.blue,
child: const Icon(Icons.history),
tooltip: 'Riwayat Penerimaan',
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.volunteer_activism,
size: 80,
color: Colors.blue.shade400,
),
),
const SizedBox(height: 24),
Text(
'Belum Ada Penerimaan',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text(
'Anda belum menerima bantuan. Bantuan akan muncul di sini ketika Anda menerimanya.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () {
controller.fetchData();
},
icon: const Icon(Icons.refresh),
label: const Text('Muat Ulang'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
);
}
Widget _buildPenerimaanList() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.penerimaPenyaluran.length,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return BantuanCard(
item: item,
onTap: () {
// Navigasi ke detail penerimaan
Get.toNamed('/warga/detail-penerimaan', arguments: {'id': item.id});
},
);
},
);
}
}

View File

@ -77,7 +77,32 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
itemCount: controller.pengaduan.length,
itemBuilder: (context, index) {
final item = controller.pengaduan[index];
final isProses = item.status == 'PROSES';
// Tentukan status dan warna berdasarkan status pengaduan
Color statusColor;
String statusText;
switch (item.status?.toUpperCase()) {
case 'PROSES':
case 'DIPROSES':
case 'TINDAKAN':
statusColor = Colors.orange;
statusText = 'Proses';
break;
case 'SELESAI':
statusColor = Colors.green;
statusText = 'Selesai';
break;
case 'DITOLAK':
statusColor = Colors.red;
statusText = 'Ditolak';
break;
default:
statusColor = Colors.grey;
statusText = item.status ?? 'Tidak Diketahui';
}
final isProses = statusText == 'Proses';
return Card(
margin: const EdgeInsets.only(bottom: 16),
@ -115,18 +140,16 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
vertical: 6,
),
decoration: BoxDecoration(
color: isProses
? Colors.orange.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isProses ? Colors.orange : Colors.green,
color: statusColor,
),
),
child: Text(
isProses ? 'Proses' : 'Selesai',
statusText,
style: TextStyle(
color: isProses ? Colors.orange : Colors.green,
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),

View File

@ -1,187 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
class WargaPenyaluranView extends GetView<WargaDashboardController> {
const WargaPenyaluranView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () async {
controller.fetchData();
},
child: controller.penerimaPenyaluran.isEmpty
? _buildEmptyState()
: _buildPenyaluranList(),
);
});
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.volunteer_activism,
size: 80,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum Ada Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
Text(
'Anda belum menerima penyaluran bantuan',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
);
}
Widget _buildPenyaluranList() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.penerimaPenyaluran.length,
itemBuilder: (context, index) {
final item = controller.penerimaPenyaluran[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
child: InkWell(
onTap: () {
// TODO: Navigasi ke detail penyaluran
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.keterangan ?? 'Bantuan',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green),
),
child: const Text(
'Diterima',
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPenerimaan!)
: '-',
style: TextStyle(
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.attach_money,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
item.jumlahBantuan != null
? currencyFormat.format(item.jumlahBantuan)
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// TODO: Implementasi navigasi ke detail penyaluran
Get.toNamed('/detail-penyaluran',
arguments: {'id': item.id});
},
icon: const Icon(Icons.visibility),
label: const Text('Lihat Detail'),
),
],
),
],
),
),
),
);
},
);
}
}

View File

@ -2,7 +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/modules/warga/views/warga_dashboard_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_penerimaan_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_pengaduan_view.dart';
import 'package:penyaluran_app/app/widgets/app_drawer.dart';
import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart';
@ -23,7 +23,7 @@ class WargaView extends GetView<WargaDashboardController> {
case 0:
return const Text('Dashboard Warga');
case 1:
return const Text('Penyaluran Bantuan');
return const Text('Penerimaan Bantuan');
case 2:
return const Text('Pengaduan');
default:
@ -96,7 +96,7 @@ class WargaView extends GetView<WargaDashboardController> {
),
DrawerMenuItem(
icon: Icons.volunteer_activism_outlined,
title: 'Penyaluran',
title: 'Penerimaan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () => controller.changeTab(1),
),
@ -125,7 +125,7 @@ class WargaView extends GetView<WargaDashboardController> {
case 0:
return const WargaDashboardView();
case 1:
return const WargaPenyaluranView();
return const WargaPenerimaanView();
case 2:
return const WargaPengaduanView();
default:
@ -144,7 +144,7 @@ class WargaView extends GetView<WargaDashboardController> {
AppBottomNavigationBarItem(
icon: Icons.volunteer_activism_outlined,
activeIcon: Icons.volunteer_activism,
label: 'Penyaluran',
label: 'Penerimaan',
badgeCount: controller.totalPenyaluranDiterima.value > 0
? controller.totalPenyaluranDiterima.value
: null,

View File

@ -22,10 +22,11 @@ import 'package:penyaluran_app/app/modules/splash/bindings/splash_binding.dart';
import 'package:penyaluran_app/app/modules/splash/views/splash_view.dart';
import 'package:penyaluran_app/app/modules/warga/bindings/warga_binding.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_dashboard_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_penerimaan_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_pengaduan_view.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_view.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/modules/warga/views/warga_detail_penerimaan_view.dart';
part 'app_routes.dart';
@ -46,12 +47,12 @@ class AppPages {
binding: AuthBinding(),
),
GetPage(
name: _Paths.wargaDashboard,
name: Routes.wargaDashboard,
page: () => const WargaView(),
binding: WargaBinding(),
),
GetPage(
name: _Paths.wargaPenyaluran,
name: Routes.wargaPenerimaan,
page: () {
final controller = Get.find<WargaDashboardController>();
controller.activeTabIndex.value = 1;
@ -60,7 +61,7 @@ class AppPages {
binding: WargaBinding(),
),
GetPage(
name: _Paths.wargaPengaduan,
name: Routes.wargaPengaduan,
page: () {
final controller = Get.find<WargaDashboardController>();
controller.activeTabIndex.value = 2;
@ -123,5 +124,10 @@ class AppPages {
page: () => DetailPenyaluranPage(),
binding: PenyaluranBinding(),
),
GetPage(
name: Routes.wargaDetailPenerimaan,
page: () => const WargaDetailPenerimaanView(),
binding: WargaBinding(),
),
];
}

View File

@ -6,7 +6,7 @@ abstract class Routes {
static const login = _Paths.login;
static const register = _Paths.register;
static const wargaDashboard = _Paths.wargaDashboard;
static const wargaPenyaluran = _Paths.wargaPenyaluran;
static const wargaPenerimaan = _Paths.wargaPenerimaan;
static const wargaPengaduan = _Paths.wargaPengaduan;
static const petugasVerifikasiDashboard = _Paths.petugasVerifikasiDashboard;
static const petugasDesaDashboard = _Paths.petugasDesaDashboard;
@ -27,6 +27,7 @@ abstract class Routes {
static const laporanPenyaluran = _Paths.laporanPenyaluran;
static const detailPenyaluran = _Paths.detailPenyaluran;
static const riwayatPenyaluran = _Paths.riwayatPenyaluran;
static const wargaDetailPenerimaan = _Paths.wargaDetailPenerimaan;
}
abstract class _Paths {
@ -35,7 +36,7 @@ abstract class _Paths {
static const login = '/login';
static const register = '/register';
static const wargaDashboard = '/warga-dashboard';
static const wargaPenyaluran = '/warga-penyaluran';
static const wargaPenerimaan = '/warga-penerimaan';
static const wargaPengaduan = '/warga-pengaduan';
static const petugasVerifikasiDashboard = '/petugas-verifikasi-dashboard';
static const petugasDesaDashboard = '/petugas-desa-dashboard';
@ -56,4 +57,5 @@ abstract class _Paths {
static const laporanPenyaluran = '/laporan-penyaluran';
static const detailPenyaluran = '/detail-penyaluran';
static const riwayatPenyaluran = '/petugas-desa/riwayat-penyaluran';
static const wargaDetailPenerimaan = '/warga/detail-penerimaan';
}

View File

@ -0,0 +1,280 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart';
class BantuanCard extends StatelessWidget {
final PenerimaPenyaluranModel item;
final VoidCallback? onTap;
final bool isCompact;
const BantuanCard({
Key? key,
required this.item,
this.onTap,
this.isCompact = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
String formattedJumlah = '';
if (item.jumlahBantuan != null) {
if (item.isUang == true) {
formattedJumlah = currencyFormat.format(item.jumlahBantuan);
} else {
formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}';
}
} else {
formattedJumlah = '-';
}
// Tampilan kompak untuk daftar ringkasan
if (isCompact) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: (item.isUang == true ? Colors.green : Colors.blue)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
item.isUang == true
? Icons.attach_money
: Icons.inventory_2,
color: item.isUang == true ? Colors.green : Colors.blue,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.namaPenyaluran ?? item.keterangan ?? 'Bantuan',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
item.kategoriNama ?? 'Bantuan',
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPenerimaan!)
: '-',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
StatusBadge(
status: item.statusPenerimaan ?? 'MENUNGGU',
fontSize: 10,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
),
const SizedBox(height: 8),
Text(
formattedJumlah,
style: TextStyle(
fontWeight: FontWeight.bold,
color: item.isUang == true ? Colors.green : Colors.blue,
fontSize: 14,
),
),
],
),
],
),
),
),
);
}
// Tampilan detail untuk halaman daftar lengkap
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.namaPenyaluran ?? item.keterangan ?? 'Bantuan',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
StatusBadge(
status: item.statusPenerimaan ?? 'MENUNGGU',
),
],
),
if (item.deskripsiPenyaluran != null &&
item.deskripsiPenyaluran!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
item.deskripsiPenyaluran!,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.category,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
item.kategoriNama ?? 'Bantuan',
style: TextStyle(
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(item.tanggalPenerimaan!)
: '-',
style: TextStyle(
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Expanded(
child: Text(
item.lokasiPenyaluranNama ?? 'Lokasi tidak tersedia',
style: TextStyle(
color: Colors.grey.shade600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
item.isUang == true
? Icons.attach_money
: Icons.inventory_2,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
formattedJumlah,
style: TextStyle(
fontWeight: FontWeight.bold,
color: item.isUang == true ? Colors.green : Colors.blue,
),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: onTap,
icon: const Icon(Icons.visibility),
label: const Text('Lihat Detail'),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class SectionHeader extends StatelessWidget {
final String title;
final VoidCallback? onViewAll;
final String? viewAllText;
final Widget? trailing;
final EdgeInsets padding;
final TextStyle? titleStyle;
const SectionHeader({
Key? key,
required this.title,
this.onViewAll,
this.viewAllText = 'Lihat Semua',
this.trailing,
this.padding = const EdgeInsets.only(bottom: 12),
this.titleStyle,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: titleStyle ??
const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (trailing != null)
trailing!
else if (onViewAll != null)
TextButton(
onPressed: onViewAll,
child: Text(viewAllText!),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
minimumSize: const Size(0, 36),
),
),
],
),
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class StatusBadge extends StatelessWidget {
final String status;
final Map<String, Color>? customColors;
final Map<String, String>? customLabels;
final double fontSize;
final EdgeInsets padding;
const StatusBadge({
Key? key,
required this.status,
this.customColors,
this.customLabels,
this.fontSize = 12,
this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
}) : super(key: key);
@override
Widget build(BuildContext context) {
final String statusUpper = status.toUpperCase();
// Default colors for common statuses
final Map<String, Color> defaultColors = {
'DITERIMA': Colors.green,
'MENUNGGU': Colors.orange,
'DITOLAK': Colors.red,
'PROSES': Colors.orange,
'DIPROSES': Colors.orange,
'TINDAKAN': Colors.orange,
'SELESAI': Colors.green,
'TERVERIFIKASI': Colors.green,
};
// Default labels for common statuses
final Map<String, String> defaultLabels = {
'DITERIMA': 'Diterima',
'MENUNGGU': 'Menunggu',
'DITOLAK': 'Ditolak',
'PROSES': 'Proses',
'DIPROSES': 'Proses',
'TINDAKAN': 'Tindakan',
'SELESAI': 'Selesai',
'TERVERIFIKASI': 'Terverifikasi',
};
// Determine color and label based on status
final Color color =
(customColors != null && customColors!.containsKey(statusUpper))
? customColors![statusUpper]!
: defaultColors.containsKey(statusUpper)
? defaultColors[statusUpper]!
: Colors.grey;
final String label =
(customLabels != null && customLabels!.containsKey(statusUpper))
? customLabels![statusUpper]!
: defaultLabels.containsKey(statusUpper)
? defaultLabels[statusUpper]!
: status;
return Container(
padding: padding,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color),
),
child: Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
),
);
}
}

View File

@ -40,7 +40,7 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Penyaluran App',
title: 'Penerimaan App',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.light, // Default ke tema terang