Perbarui dependensi dengan menambahkan paket percent_indicator versi 4.2.4. Modifikasi file pubspec.yaml dan pubspec.lock untuk mencerminkan perubahan ini. Selain itu, perbarui status penerimaan di PelaksanaanPenyaluranController dari 'SUDAHMENERIMA' menjadi 'DITERIMA' untuk konsistensi. Tambahkan fungsionalitas baru di PetugasDesaDashboardController untuk memuat jadwal hari ini dan total penitipan terverifikasi. Perbarui tampilan di beberapa view untuk meningkatkan pengalaman pengguna dan konsistensi data.

This commit is contained in:
Khafidh Fuadi
2025-03-25 21:03:40 +07:00
parent 32736be867
commit 3b963178f4
20 changed files with 2191 additions and 818 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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><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  <08>ّ<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><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>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><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>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><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>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><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>2y
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <08><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  <08>ّ<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><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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD>
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt  <08>ݑ<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><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>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><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>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><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>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><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>2{
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  <08>ޑ<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><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  <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{
y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  <EFBFBD><EFBFBD><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  ؔ<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  <EFBFBD><EFBFBD><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  ؔ<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  ؔ<><D894><EFBFBD>2 x
v
v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  ؔ<><D894><EFBFBD>2
~
|
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  ؔ<><D894><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  ؔ<><D894><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

@ -287,7 +287,7 @@ class PelaksanaanPenyaluranController extends GetxController {
// Metode untuk mendapatkan warna status penerimaan
Color getStatusColor(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
case 'DITERIMA':
return AppTheme.successColor;
case 'BELUMMENERIMA':
return AppTheme.warningColor;
@ -299,7 +299,7 @@ class PelaksanaanPenyaluranController extends GetxController {
// Metode untuk mendapatkan ikon status penerimaan
IconData getStatusIcon(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
case 'DITERIMA':
return Icons.check_circle;
case 'BELUMMENERIMA':
return Icons.event_available;
@ -311,7 +311,7 @@ class PelaksanaanPenyaluranController extends GetxController {
// Metode untuk mendapatkan teks status penerimaan
String getStatusText(String status) {
switch (status.toUpperCase()) {
case 'SUDAHMENERIMA':
case 'DITERIMA':
return 'Sudah Menerima';
case 'BELUMMENERIMA':
return 'Belum Menerima';
@ -373,7 +373,7 @@ class PelaksanaanPenyaluranController extends GetxController {
createdAt: penerimaPenyaluran[index].createdAt,
penyaluranBantuanId: penerimaPenyaluran[index].penyaluranBantuanId,
wargaId: penerimaPenyaluran[index].wargaId,
statusPenerimaan: 'SUDAHMENERIMA',
statusPenerimaan: 'DITERIMA',
tanggalPenerimaan: penerimaPenyaluran[index].tanggalPenerimaan,
buktiPenerimaan: penerimaPenyaluran[index].buktiPenerimaan,
keterangan: penerimaPenyaluran[index].keterangan,

View File

@ -18,12 +18,18 @@ class PetugasDesaDashboardController extends GetxController {
final RxInt totalPenerima = 0.obs;
final RxInt totalBantuan = 0.obs;
final RxInt totalPenyaluran = 0.obs;
final RxInt totalSemuaPenyaluran = 0.obs;
final RxInt totalPenitipanTerverifikasi = 0.obs;
final RxDouble progressPenyaluran = 0.0.obs;
// Data untuk notifikasi
final RxList<NotifikasiModel> notifikasiBelumDibaca = <NotifikasiModel>[].obs;
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Data untuk jadwal hari ini
final RxList<Map<String, dynamic>> jadwalHariIni =
<Map<String, dynamic>>[].obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
@ -45,6 +51,7 @@ class PetugasDesaDashboardController extends GetxController {
loadUserProfile();
loadDashboardData();
loadNotifikasiData();
loadJadwalHariIni();
}
@override
@ -76,18 +83,24 @@ class PetugasDesaDashboardController extends GetxController {
final penerimaData = await _supabaseService.getTotalPenerima();
totalPenerima.value = penerimaData ?? 0;
// Mengambil data total bantuan
final bantuanData = await _supabaseService.getTotalBantuan();
totalBantuan.value = bantuanData ?? 0;
// Mengambil data total penitipan terverifikasi
final penitipanData =
await _supabaseService.getTotalPenitipanTerverifikasi();
totalPenitipanTerverifikasi.value = penitipanData ?? 0;
// Mengambil data total penyaluran
// Mengambil data total penyaluran terlaksana
final penyaluranData = await _supabaseService.getTotalPenyaluran();
totalPenyaluran.value = penyaluranData ?? 0;
// Menghitung progress penyaluran
if (totalBantuan.value > 0) {
// Mengambil data total semua penyaluran
final semuaPenyaluranData =
await _supabaseService.getTotalSemuaPenyaluran();
totalSemuaPenyaluran.value = semuaPenyaluranData ?? 0;
// Menghitung progress penyaluran (persentase penyaluran yang terlaksana dari total semua penyaluran)
if (totalSemuaPenyaluran.value > 0) {
progressPenyaluran.value =
(totalPenyaluran.value / totalBantuan.value) * 100;
(totalPenyaluran.value / totalSemuaPenyaluran.value) * 100;
} else {
progressPenyaluran.value = 0.0;
}
@ -114,11 +127,28 @@ class PetugasDesaDashboardController extends GetxController {
}
}
Future<void> loadJadwalHariIni() async {
try {
final jadwalData = await _supabaseService.getJadwalHariIni();
if (jadwalData != null) {
jadwalHariIni.value = jadwalData;
}
} catch (e) {
print('Error loading jadwal hari ini: $e');
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadDashboardData();
await loadNotifikasiData();
await Future.wait([
loadUserProfile(),
loadDashboardData(),
loadNotifikasiData(),
loadJadwalHariIni(),
]);
} catch (e) {
print('Error refreshing data: $e');
} finally {
isLoading.value = false;
}

View File

@ -1,101 +1,301 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/greeting_header.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/progress_section.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/schedule_card.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/cards/statistic_card.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
class DashboardView extends GetView<PetugasDesaController> {
class DashboardView extends GetView<PetugasDesaDashboardController> {
const DashboardView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return RefreshIndicator(
onRefresh: () => controller.refreshData(),
child: Obx(() => AnimationLimiter(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: AnimationConfiguration.staggeredList(
position: 0,
delay: const Duration(milliseconds: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan greeting
FadeInAnimation(
child: GreetingHeader(
name: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.desa,
),
),
const SizedBox(height: 20),
// Jadwal penyaluran hari ini
FadeInAnimation(
delay: const Duration(milliseconds: 300),
child: _buildJadwalHariIni(),
),
const SizedBox(height: 20),
// Progress penyaluran
FadeInAnimation(
delay: const Duration(milliseconds: 400),
child: _buildProgressPenyaluran(),
),
const SizedBox(height: 20),
// Statistik performa desa
FadeInAnimation(
delay: const Duration(milliseconds: 500),
child: _buildStatistikPerforma(),
),
const SizedBox(height: 20),
// Daftar penerima terbaru
FadeInAnimation(
delay: const Duration(milliseconds: 600),
child: _buildRecipientsList(textTheme),
),
],
),
),
),
),
)),
);
}
Widget _buildJadwalHariIni() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 12),
FutureBuilder<List<Map<String, dynamic>>?>(
future: SupabaseService.to.getJadwalHariIni(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Gagal memuat jadwal'));
}
final jadwalList = snapshot.data;
if (jadwalList == null || jadwalList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.event_busy, color: Colors.grey),
SizedBox(width: 8),
Text('Tidak ada jadwal penyaluran hari ini'),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: jadwalList.length,
itemBuilder: (context, index) {
final jadwal = jadwalList[index];
final DateTime tanggal =
DateTime.parse(jadwal['tanggal_penyaluran']);
final String formattedDate =
DateTimeHelper.formatDateTime(tanggal);
final kategoriBantuan =
jadwal['kategori_bantuan'] as Map<String, dynamic>;
final lokasiPenyaluran =
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
return ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
);
},
);
},
),
],
);
}
Widget _buildProgressPenyaluran() {
// Menghitung nilai untuk progress
final terlaksana = controller.totalPenyaluran.value;
final total = controller.totalSemuaPenyaluran.value;
final progressValue = total > 0 ? terlaksana / total : 0.0;
final belumTerlaksana = total - terlaksana;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Progress Penyaluran',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
Row(
children: [
CircularPercentIndicator(
radius: 60.0,
lineWidth: 10.0,
percent: progressValue > 1.0 ? 1.0 : progressValue,
center: Text(
'${(progressValue * 100).toInt()}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
progressColor: Colors.white,
backgroundColor: Colors.white.withOpacity(0.2),
circularStrokeCap: CircularStrokeCap.round,
animation: true,
animationDuration: 1200,
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProgressDetailItem(
'Telah Terlaksana',
'$terlaksana',
Colors.white,
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Belum Terlaksana',
'$belumTerlaksana',
Colors.white.withOpacity(0.7),
),
const SizedBox(height: 8),
_buildProgressDetailItem(
'Total Penyaluran',
'$total',
Colors.white,
),
],
),
),
],
),
],
),
);
}
Widget _buildProgressDetailItem(String label, String value, Color color) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: color,
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
Widget _buildStatistikPerforma() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistik Performa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 12),
Row(
children: [
// Header dengan greeting
GreetingHeader(
name: controller.namaLengkap,
role: 'Petugas Desa',
desa: controller.desa,
Expanded(
child: StatisticCard(
title: 'Penitipan',
count: controller.jumlahNotifikasiBelumDibaca.toString(),
subtitle: 'Perlu Konfirmasi',
height: 120,
icon: Icons.inbox,
),
),
const SizedBox(height: 20),
// Jadwal penyaluran hari ini
ScheduleCard(
title: 'Jadwal Penyaluran Hari ini',
location: 'Kantor Kepala Desa (Beras)',
dateTime: '15 April 2023, 13:00 - 14:00',
isToday: true,
onTap: () => Get.toNamed('/petugas-desa/jadwal'),
),
const SizedBox(height: 20),
// Jadwal penyaluran mendatang
ScheduleCard(
title: 'Jadwal Penyaluran Mendatang',
location: 'Balai Desa A (Sembako)',
dateTime: '17 April 2023, 13:00 - 14:00',
isToday: false,
onTap: () => Get.toNamed('/petugas-desa/jadwal'),
),
const SizedBox(height: 20),
// Statistik penyaluran
Row(
children: [
Expanded(
child: StatisticCard(
title: 'Penitipan',
count: '3',
subtitle: 'Perlu Konfirmasi',
height: 120,
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Pengaduan',
count:
'${controller.totalPenerima.value > 0 ? controller.totalPenerima.value ~/ 10 : 0}',
subtitle: 'Perlu Tindakan',
height: 120,
gradient: LinearGradient(
colors: [Colors.orange, Colors.deepOrange],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Penjadwalan',
count: '1',
subtitle: 'Perlu Konfirmasi',
height: 120,
),
),
const SizedBox(width: 10),
Expanded(
child: StatisticCard(
title: 'Pengaduan',
count: '1',
subtitle: 'Perlu Tindakan',
height: 120,
),
),
],
icon: Icons.warning_amber,
),
),
const SizedBox(height: 20),
// Progress penyaluran
ProgressSection(
progressValue: 0.7,
total: 100,
distributed: 70,
scheduled: 20,
unscheduled: 10,
),
const SizedBox(height: 20),
// Daftar penerima
_buildRecipientsList(textTheme),
],
),
),
],
);
}
@ -107,15 +307,16 @@ class DashboardView extends GetView<PetugasDesaController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Penerima',
style: textTheme.titleMedium?.copyWith(
fontSize: 16,
'Daftar Penerima Terbaru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
TextButton(
onPressed: () {
Get.toNamed('/daftar-penerima');
Get.toNamed(Routes.daftarPenerima);
},
child: Row(
children: [
@ -136,65 +337,121 @@ class DashboardView extends GetView<PetugasDesaController> {
],
),
const SizedBox(height: 10),
_buildRecipientItem(
'Siti Rahayu', '3201020107030011', 'Selesai', textTheme),
_buildRecipientItem(
'Budi Santoso', '3201020107030012', 'Selesai', textTheme),
_buildRecipientItem(
'Dewi Lestari', '3201020107030013', 'Selesai', textTheme),
FutureBuilder<List<Map<String, dynamic>>?>(
future: SupabaseService.to.getPenerimaTerbaru(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Gagal memuat data penerima'));
}
final penerimaList = snapshot.data;
if (penerimaList == null || penerimaList.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.person_off, color: Colors.grey),
SizedBox(width: 8),
Text('Belum ada data penerima'),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: penerimaList.length > 3 ? 3 : penerimaList.length,
itemBuilder: (context, index) {
final penerima = penerimaList[index];
final name = penerima['nama_lengkap'] ?? 'Nama tidak tersedia';
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
final status = penerima['status'] ?? 'AKTIF';
final id = penerima['id'] ?? 'ID tidak tersedia';
return _buildRecipientItem(name, nik, status, id, textTheme);
},
);
},
),
],
);
}
Widget _buildRecipientItem(
String name, String nik, String status, TextTheme textTheme) {
String name, String nik, String status, String id, TextTheme textTheme) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: () {
// Navigasi ke detail penerima dengan ID statis
// Kita gunakan ID 1 untuk Siti Rahayu, 2 untuk Budi Santoso, 3 untuk Dewi Lestari
String id = "1"; // Default
if (nik == "3201020107030011") {
id = "2";
} else if (nik == "3201020107030012") {
id = "3";
}
Get.toNamed('/daftar-penerima/detail', arguments: id);
Get.toNamed(Routes.detailPenerima, arguments: id);
},
borderRadius: BorderRadius.circular(12),
child: ListTile(
title: Text(
name,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
subtitle: Text(
'NIK: $nik',
style: textTheme.bodyMedium?.copyWith(
color: Colors.white,
),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textTheme.bodySmall?.copyWith(
color: Colors.white,
fontSize: 12,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2),
child: const Icon(Icons.person, color: Colors.white),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'NIK: $nik',
style: textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: textTheme.bodySmall?.copyWith(
color: Colors.white,
fontSize: 12,
),
),
),
],
),
),
),

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class DetailPenerimaView extends GetView<PenerimaController> {
const DetailPenerimaView({super.key});
@ -54,9 +55,6 @@ class DetailPenerimaView extends GetView<PenerimaController> {
// Detail informasi penerima
_buildDetailInfo(penerima),
// Status penyaluran
_buildStatusSection(penerima),
// Riwayat Penyaluran Bantuan
_buildRiwayatPenyaluran(),
@ -127,7 +125,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
// Nama penerima dengan stroke effect
Text(
penerima['nama'] ?? '',
penerima['nama_lengkap'] ?? '',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
@ -203,7 +201,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
),
// Informasi status aktif
const SizedBox(height: 12),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
@ -255,27 +253,26 @@ class DetailPenerimaView extends GetView<PenerimaController> {
children: [
_buildInfoRow('NIK', penerima['nik'] ?? '-'),
const Divider(),
_buildInfoRow('No KK', penerima['noKK'] ?? '-'),
_buildInfoRow('No KK', penerima['no_kk'] ?? '-'),
const Divider(),
_buildInfoRow('No Handphone', penerima['noHandphone'] ?? '-'),
_buildInfoRow('No Handphone', penerima['no_hp'] ?? '-'),
const Divider(),
_buildInfoRow('Email', penerima['email'] ?? '-'),
const Divider(),
_buildInfoRow(
'Jenis Kelamin', penerima['jenisKelamin'] ?? '-'),
'Jenis Kelamin', penerima['jenis_kelamin'] ?? '-'),
const Divider(),
_buildInfoRow('Agama', penerima['agama'] ?? '-'),
const Divider(),
_buildInfoRow('Tempat, Tanggal Lahir',
penerima['tempatTanggalLahir'] ?? '-'),
const Divider(),
_buildInfoRow(
'Alamat Lengkap', penerima['alamatLengkap'] ?? '-'),
'Tempat, Tanggal Lahir', penerima['tempat_lahir'] ?? '-'),
const Divider(),
_buildInfoRow('Alamat Lengkap', penerima['alamat'] ?? '-'),
const Divider(),
_buildInfoRow('Pekerjaan', penerima['pekerjaan'] ?? '-'),
const Divider(),
_buildInfoRow('Pendidikan Terakhir',
penerima['pendidikanTerakhir'] ?? '-'),
_buildInfoRow(
'Pendidikan Terakhir', penerima['pendidikan'] ?? '-'),
],
),
),
@ -315,111 +312,6 @@ class DetailPenerimaView extends GetView<PenerimaController> {
);
}
Widget _buildStatusSection(Map<String, dynamic> penerima) {
Color statusColor;
IconData statusIcon;
switch (penerima['status']) {
case 'Selesai':
statusColor = AppTheme.completedColor;
statusIcon = Icons.check_circle;
break;
case 'Terjadwal':
statusColor = AppTheme.processedColor;
statusIcon = Icons.event;
break;
case 'Belum disalurkan':
statusColor = AppTheme.warningColor;
statusIcon = Icons.pending;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help;
}
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
statusIcon,
color: statusColor,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
penerima['status'] ?? 'Tidak diketahui',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
if (penerima['status'] == 'Belum disalurkan')
const Text(
'Penerima ini belum dijadwalkan penyaluran bantuan',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
if (penerima['status'] == 'Terjadwal')
const Text(
'Penerima ini sudah dijadwalkan penyaluran bantuan',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
if (penerima['status'] == 'Selesai')
const Text(
'Penerima ini sudah menerima bantuan',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
],
),
),
),
],
),
);
}
// Widget untuk menampilkan riwayat penyaluran bantuan
Widget _buildRiwayatPenyaluran() {
return Container(
@ -436,6 +328,11 @@ class DetailPenerimaView extends GetView<PenerimaController> {
),
const SizedBox(height: 16),
Obx(() {
// Debug prints
print('Loading state: ${controller.isLoadingPenyaluran.value}');
print(
'Daftar penyaluran length: ${controller.daftarPenyaluran.length}');
if (controller.isLoadingPenyaluran.value) {
return const Center(
child: Padding(
@ -475,8 +372,18 @@ class DetailPenerimaView extends GetView<PenerimaController> {
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.daftarPenyaluran.length,
itemBuilder: (context, index) {
final penyaluran = controller.daftarPenyaluran[index];
return _buildPenyaluranItem(penyaluran);
try {
final penyaluran = controller.daftarPenyaluran[index];
return _buildPenyaluranItem(penyaluran);
} catch (e) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Terjadi kesalahan: $e'),
),
);
}
},
);
}),
@ -487,137 +394,98 @@ class DetailPenerimaView extends GetView<PenerimaController> {
// Widget untuk menampilkan item penyaluran bantuan
Widget _buildPenyaluranItem(Map<String, dynamic> penyaluran) {
final DateTime tanggalPenyaluran =
DateTime.parse(penyaluran['tanggal_penyaluran']);
final String formattedDate =
DateFormat('dd MMMM yyyy', 'id_ID').format(tanggalPenyaluran);
// Status penerimaan dengan nilai default
final String statusPenerimaan =
penyaluran['status_penerimaan'] ?? 'BELUMMENERIMA';
final Color statusColor = penyaluran['status'] == 'TERLAKSANA'
final Color statusColor = statusPenerimaan == 'DITERIMA'
? AppTheme.completedColor
: penyaluran['status'] == 'DIJADWALKAN'
: statusPenerimaan == 'BELUMMENERIMA'
? AppTheme.processedColor
: AppTheme.warningColor;
final IconData statusIcon = penyaluran['status'] == 'TERLAKSANA'
final IconData statusIcon = statusPenerimaan == 'DITERIMA'
? Icons.check_circle
: penyaluran['status'] == 'DIJADWALKAN'
? Icons.event
: Icons.pending;
: statusPenerimaan == 'BELUMMENERIMA'
? Icons.hourglass_empty
: Icons.help;
final Map<String, dynamic> stokBantuan =
penyaluran['stok_bantuan'] as Map<String, dynamic>;
// Data penyaluran bantuan
final Map<String, dynamic> penyaluranBantuan =
penyaluran['penyaluran_bantuan'] as Map<String, dynamic>? ?? {};
// Format tanggal menggunakan DateTimeHelper
final tanggalPenerimaan = penyaluran['tanggal_penerimaan'] != null
? DateTime.parse(penyaluran['tanggal_penerimaan'])
: null;
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Baris atas dengan status dan tanggal
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Status penyaluran
Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
statusIcon,
color: statusColor,
size: 16,
),
),
const SizedBox(width: 8),
Text(
penyaluran['status'] == 'TERLAKSANA'
? 'Terlaksana'
: penyaluran['status'] == 'DIJADWALKAN'
? 'Terjadwal'
: 'Menunggu',
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
// Tanggal penyaluran
Row(
children: [
const Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 8),
Text(
formattedDate,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan nama program dan status
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
const Divider(height: 24),
// Informasi bantuan
Row(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
// Ikon bantuan
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.inventory_2_outlined,
color: AppTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 16),
// Detail bantuan
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stokBantuan['nama'] ?? 'Bantuan',
penyaluranBantuan['nama'] ?? 'Program Bantuan',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
if (penyaluranBantuan['deskripsi'] != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
penyaluranBantuan['deskripsi'],
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 16,
color: statusColor,
),
const SizedBox(width: 6),
Text(
'${stokBantuan['jenis'] ?? 'Umum'}${stokBantuan['kuantitas'] ?? '1 Paket'}',
statusPenerimaan,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 8),
Text(
penyaluran['keterangan'] ?? '',
style: const TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
@ -626,45 +494,255 @@ class DetailPenerimaView extends GetView<PenerimaController> {
),
],
),
),
// Tampilkan bukti penyaluran jika ada dan status TERLAKSANA
if (penyaluran['status'] == 'TERLAKSANA' &&
penyaluran['bukti_penyaluran'] != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 24),
const Text(
'Bukti Penyaluran',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi waktu dan jumlah
Row(
children: [
Expanded(
child: _buildInfoItem(
Icons.calendar_today,
'Tanggal Penerimaan',
DateTimeHelper.formatDateTime(tanggalPenerimaan),
),
),
Expanded(
child: _buildInfoItem(
Icons.inventory_2,
'Jumlah Diterima',
'${penyaluran['jumlah_bantuan'] ?? '0'} paket',
),
),
],
),
const SizedBox(height: 16),
if (penyaluranBantuan['lokasi_penyaluran'] != null) ...[
Row(
children: [
Expanded(
child: _buildInfoItem(
Icons.location_on,
'Lokasi Penyaluran',
penyaluranBantuan['lokasi_penyaluran']['nama'] ??
'Tidak tersedia',
),
),
Expanded(
child: _buildInfoItem(
Icons.map,
'Alamat Lokasi',
penyaluranBantuan['lokasi_penyaluran']
['alamat_lengkap'] ??
'Tidak tersedia',
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
penyaluran['bukti_penyaluran'],
height: 120,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 120,
width: double.infinity,
color: Colors.grey[200],
child: const Center(
child: Text('Gambar tidak tersedia'),
],
const SizedBox(height: 16),
// Bukti penerimaan dan tanda tangan
if (penyaluran['bukti_penerimaan'] != null ||
penyaluran['tanda_tangan'] != null)
Row(
children: [
if (penyaluran['bukti_penerimaan'] != null)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Bukti Penerimaan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
penyaluran['bukti_penerimaan'],
height: 100,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 100,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(
Icons.broken_image,
color: Colors.grey,
),
),
);
},
),
),
],
),
);
},
),
if (penyaluran['bukti_penerimaan'] != null &&
penyaluran['tanda_tangan'] != null)
const SizedBox(width: 16),
if (penyaluran['tanda_tangan'] != null)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tanda Tangan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
penyaluran['tanda_tangan'],
height: 100,
width: double.infinity,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 100,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(
Icons.broken_image,
color: Colors.grey,
),
),
);
},
),
),
],
),
),
],
),
// QR Code
if (penyaluran['qr_code_hash'] != null) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Center(
child: Column(
children: [
const Text(
'QR Code Verifikasi',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Image.network(
'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${penyaluran['qr_code_hash']}',
height: 120,
width: 120,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Icon(
Icons.qr_code_2,
color: Colors.grey,
size: 40,
),
),
);
},
),
),
],
),
),
],
),
],
),
],
),
),
],
),
);
}
Widget _buildInfoItem(IconData icon, String label, String value) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 20,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
}

View File

@ -1744,8 +1744,8 @@ class DetailPenyaluranPage extends StatelessWidget {
if (status == 'DITERIMA') {
return item.statusPenerimaan?.toUpperCase() == 'DITERIMA';
} else {
// Filter untuk yang belum menerima
return item.statusPenerimaan?.toUpperCase() != 'DITERIMA';
// Semua status selain DITERIMA dianggap sebagai BELUMMENERIMA
return item.statusPenerimaan?.toUpperCase() == 'BELUMMENERIMA';
}
}).toList();
}

View File

@ -46,6 +46,9 @@ class ProfileController extends GetxController {
Future<void> loadUserData() async {
isLoading.value = true;
try {
// Hapus cache data user sebelum mengambil data baru
_supabaseService.clearUserProfileCache();
// Mendapatkan data user dari service
final userData = await _supabaseService.getUserProfile();
if (userData != null) {
@ -67,9 +70,15 @@ class ProfileController extends GetxController {
if (roleData.value?['foto_profil'] != null) {
fotoProfil.value = roleData.value?['foto_profil'] ?? '';
print(fotoProfil.value);
} else {
// Reset foto profil jika tidak ada data
fotoProfil.value = '';
}
}
}
// Muat ulang data user di AuthController untuk memastikan konsistensi
await _authController.refreshUserData();
} catch (e) {
Get.snackbar(
'Error',
@ -140,6 +149,95 @@ class ProfileController extends GetxController {
// Metode untuk menghapus foto profil
void clearFotoProfil() {
fotoProfilPath.value = '';
if (isEditing.value) {
// Cek jika user adalah warga
if (user.value?.role?.toLowerCase() == 'warga') {
Get.snackbar(
'Tidak Diizinkan',
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber,
colorText: Colors.black,
duration: const Duration(seconds: 5),
);
return;
}
// Tandai bahwa foto profil akan dihapus saat menyimpan perubahan
Get.dialog(
AlertDialog(
title: const Text('Konfirmasi'),
content: const Text('Apakah Anda yakin ingin menghapus foto profil?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () async {
Get.back();
try {
final userData = user.value;
if (userData == null) return;
// Update data profil dengan foto kosong
switch (userData.role?.toLowerCase() ?? 'unknown') {
case 'donatur':
await _supabaseService.updateDonaturProfile(
userId: userData.id,
nama: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil:
'', // Kosongkan foto profil dengan string kosong
);
break;
case 'petugas_desa':
await _supabaseService.updatePetugasDesaProfile(
userId: userData.id,
nama: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil:
'', // Kosongkan foto profil dengan string kosong
);
break;
default:
break;
}
// Hapus cache dan refresh data
_supabaseService.clearUserProfileCache();
fotoProfil.value = '';
await _authController.refreshUserData();
await loadUserData();
Get.snackbar(
'Sukses',
'Foto profil berhasil dihapus',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Error',
'Gagal menghapus foto profil: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Hapus'),
),
],
),
);
}
}
// Metode untuk mengupload foto profil
@ -179,11 +277,34 @@ class ProfileController extends GetxController {
return;
}
final userData = user.value;
if (userData == null) {
Get.snackbar(
'Error',
'Data user tidak ditemukan',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Cek jika user adalah warga, maka tidak diperbolehkan mengubah profil
if (userData.role?.toLowerCase() == 'warga') {
Get.snackbar(
'Tidak Diizinkan',
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber,
colorText: Colors.black,
duration: const Duration(seconds: 5),
);
isEditing.value = false;
return;
}
isLoading.value = true;
try {
final userData = user.value;
if (userData == null) throw 'Data user tidak ditemukan';
// Upload foto profil jika ada
String? fotoProfilUrl;
if (fotoProfilPath.isNotEmpty) {
@ -195,15 +316,6 @@ class ProfileController extends GetxController {
// Update data sesuai role
switch (userData.role?.toLowerCase() ?? 'unknown') {
case 'warga':
await _supabaseService.updateWargaProfile(
userId: userData.id,
namaLengkap: nameController.text,
noHp: phoneController.text,
email: emailController.text,
fotoProfil: fotoProfilUrl,
);
break;
case 'donatur':
await _supabaseService.updateDonaturProfile(
userId: userData.id,
@ -226,14 +338,17 @@ class ProfileController extends GetxController {
throw 'Role tidak valid';
}
// Refresh data lokal
await loadUserData();
// Hapus cache data profil sebelum refresh
_supabaseService.clearUserProfileCache();
// Reset path foto setelah update
fotoProfilPath.value = '';
// Refresh data di AuthController untuk menyebarkan perubahan ke seluruh aplikasi
await _authController.refreshUserData();
// Reset path foto setelah update
fotoProfilPath.value = '';
// Refresh data lokal
await loadUserData();
// Keluar dari mode edit
isEditing.value = false;

View File

@ -58,17 +58,22 @@ class ProfileView extends GetView<ProfileController> {
title: const Text('Profil'),
actions: [
Obx(() {
if (controller.isEditing.value) {
return IconButton(
icon: const Icon(Icons.save),
onPressed: controller.updateProfile,
);
} else {
return IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.toggleEditMode,
);
// Hanya tampilkan tombol edit jika user bukan warga
if (controller.user.value?.role?.toLowerCase() != 'warga') {
if (controller.isEditing.value) {
return IconButton(
icon: const Icon(Icons.save),
onPressed: controller.updateProfile,
);
} else {
return IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.toggleEditMode,
);
}
}
return const SizedBox
.shrink(); // Jangan tampilkan apapun untuk warga
}),
],
),
@ -122,7 +127,8 @@ class ProfileView extends GetView<ProfileController> {
// Tombol edit foto (hanya muncul dalam mode edit)
Obx(() {
if (controller.isEditing.value) {
if (controller.isEditing.value &&
controller.user.value?.role?.toLowerCase() != 'warga') {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
@ -164,13 +170,36 @@ class ProfileView extends GetView<ProfileController> {
}),
const SizedBox(height: 16),
Obx(() => Text(
controller.user.value?.name ?? 'Pengguna',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
)),
// Menggunakan Obx untuk reaktif memperbarui nama
Obx(() {
// Mengambil nama dari roleData jika ada, atau dari user
String displayName = '';
final roleDataValue = controller.roleData.value;
final userValue = controller.user.value;
if (roleDataValue != null) {
// Prioritaskan data dari roleData karena lebih spesifik
if (roleDataValue['nama_lengkap'] != null) {
displayName = roleDataValue['nama_lengkap'];
} else if (roleDataValue['nama'] != null) {
displayName = roleDataValue['nama'];
}
}
// Gunakan data dari user jika tidak ada di roleData
if (displayName.isEmpty && userValue != null) {
displayName = userValue.name ?? 'Pengguna';
}
return Text(
displayName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
}),
const SizedBox(height: 4),
Obx(() {
final role = controller.user.value?.role;
@ -235,8 +264,10 @@ class ProfileView extends GetView<ProfileController> {
fit: BoxFit.cover,
width: 120,
height: 120,
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
errorBuilder: (context, error, stackTrace) {
print('Error loading local image: $error');
return _buildDefaultProfileImage();
},
)
: Image.network(
imagePath,
@ -254,8 +285,11 @@ class ProfileView extends GetView<ProfileController> {
),
);
},
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
errorBuilder: (context, error, stackTrace) {
print('Error loading network image: $error');
print('Failed image URL: $imagePath');
return _buildDefaultProfileImage();
},
),
),
),
@ -321,157 +355,273 @@ class ProfileView extends GetView<ProfileController> {
return Obx(() {
final isEditing = controller.isEditing.value;
final user = controller.user.value;
// Form tidak bisa diedit jika usernya warga
final bool canEdit = isEditing && user?.role?.toLowerCase() != 'warga';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
),
const SizedBox(height: 16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person_outline, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
// Nama
TextField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
enabled: isEditing,
),
),
const SizedBox(height: 16),
// Menampilkan notifikasi khusus untuk warga
if (user?.role?.toLowerCase() == 'warga') ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.withOpacity(0.5)),
),
child: Row(
children: [
Icon(Icons.info_outline,
color: Colors.amber[800], size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
'Data warga hanya dapat diubah melalui aplikasi verifikasi data warga. Silakan hubungi petugas desa untuk perubahan data.',
style: TextStyle(
fontSize: 14,
color: Colors.amber[900],
),
),
),
],
),
),
],
// Email
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
enabled: false, // Email tidak bisa diubah
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Nomor Telepon
TextField(
controller: controller.phoneController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
enabled: isEditing,
// Nama
TextField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.person),
enabled: canEdit,
),
),
const SizedBox(height: 16),
// Email
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.email),
enabled: false, // Email tidak bisa diubah
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
// Nomor Telepon
TextField(
controller: controller.phoneController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(Icons.phone),
enabled: canEdit,
),
keyboardType: TextInputType.phone,
),
],
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
// Informasi tambahan sesuai role
if (user != null) ...[
if (user.role?.toLowerCase() == 'warga') ...[
const SizedBox(height: 24),
const Text(
'Informasi Warga',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.infoColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
Icons.perm_identity, 'NIK', roleData?['nik'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(Icons.wc, 'Jenis Kelamin',
roleData?['jenis_kelamin'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(
Icons.home, 'Alamat', roleData?['alamat'] ?? '-'),
if (user.desa != null) ...[
const SizedBox(height: 8),
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
Row(
children: [
Icon(Icons.person_pin_circle,
color: AppTheme.infoColor),
const SizedBox(width: 10),
const Text(
'Informasi Lainnya',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.perm_identity, 'NIK',
roleData?['nik'] ?? '-'),
_buildInfoRow(Icons.wc, 'Jenis Kelamin',
roleData?['jenis_kelamin'] ?? '-'),
_buildInfoRow(
Icons.home, 'Alamat', roleData?['alamat'] ?? '-'),
if (user.desa != null) ...[
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
],
);
}),
],
);
}),
),
),
],
if (user.role?.toLowerCase() == 'donatur') ...[
const SizedBox(height: 24),
const Text(
'Informasi Donatur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.successColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.business, 'Instansi',
roleData?['instansi'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(
Icons.work, 'Jabatan', roleData?['jabatan'] ?? '-'),
Row(
children: [
Icon(Icons.volunteer_activism,
color: AppTheme.successColor),
const SizedBox(width: 10),
const Text(
'Informasi Donatur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.business, 'Instansi',
roleData?['instansi'] ?? '-'),
_buildInfoRow(Icons.work, 'Jabatan',
roleData?['jabatan'] ?? '-'),
],
);
}),
],
);
}),
),
),
],
if (user.role?.toLowerCase() == 'petugas_desa') ...[
const SizedBox(height: 24),
const Text(
'Informasi Petugas Desa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Container(
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: AppTheme.primaryColor.withOpacity(0.2)),
),
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.badge, 'NIP', roleData?['nip'] ?? '-'),
if (user.desa != null) ...[
const SizedBox(height: 8),
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
const SizedBox(height: 8),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
Row(
children: [
Icon(Icons.badge, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Informasi Petugas Desa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Obx(() {
final roleData = controller.roleData.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
Icons.badge, 'NIP', roleData?['nip'] ?? '-'),
if (user.desa != null) ...[
_buildInfoRow(
Icons.location_city, 'Desa', user.desa!.nama),
_buildInfoRow(Icons.location_on, 'Kecamatan',
user.desa!.kecamatan ?? ''),
_buildInfoRow(Icons.location_on, 'Kabupaten',
user.desa!.kabupaten ?? ''),
_buildInfoRow(Icons.location_on, 'Provinsi',
user.desa!.provinsi ?? ''),
],
],
);
}),
],
);
}),
),
),
],
],
],
@ -480,28 +630,54 @@ class ProfileView extends GetView<ProfileController> {
}
Widget _buildPasswordSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Keamanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showChangePasswordDialog(context),
icon: const Icon(Icons.lock),
label: const Text('Ubah Password'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.security, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text(
'Keamanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showChangePasswordDialog(context),
icon: const Icon(Icons.lock),
label: const Text('Ubah Password'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
],
),
);
}
@ -512,34 +688,52 @@ class ProfileView extends GetView<ProfileController> {
Get.dialog(
AlertDialog(
title: const Text('Ubah Password'),
title: Row(
children: [
Icon(Icons.lock_reset, color: AppTheme.primaryColor),
const SizedBox(width: 10),
const Text('Ubah Password'),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: currentPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Password Saat Ini',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.key),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: newPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Password Baru',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.lock),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: confirmPasswordController,
decoration: const InputDecoration(
decoration: InputDecoration(
labelText: 'Konfirmasi Password Baru',
border: OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.lock_clock),
),
obscureText: true,
),
@ -550,6 +744,9 @@ class ProfileView extends GetView<ProfileController> {
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey[700],
),
),
ElevatedButton(
onPressed: () {
@ -561,6 +758,11 @@ class ProfileView extends GetView<ProfileController> {
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text('Simpan'),
),
@ -570,21 +772,65 @@ class ProfileView extends GetView<ProfileController> {
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: Colors.grey[700]),
const SizedBox(width: 8),
Expanded(
child: Text(
'$label: $value',
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
return Container(
margin: const EdgeInsets.only(bottom: 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),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 22,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value.isEmpty ? '-' : value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
],
),
);
}
}

View File

@ -106,6 +106,16 @@ class WargaDashboardController extends GetxController {
super.onInit();
fetchData();
loadUserData();
// Atau gunakan timer untuk refresh data secara periodik
// Timer.periodic(Duration(seconds: 60), (_) => loadUserData());
}
@override
void onReady() {
super.onReady();
// Perbarui data user dan foto profil saat halaman siap
loadUserData();
}
void loadUserData() {
@ -133,7 +143,7 @@ class WargaDashboardController extends GetxController {
wargaData.fotoProfil != null &&
wargaData.fotoProfil!.isNotEmpty) {
fotoProfil.value = wargaData.fotoProfil!;
print('DEBUG WARGA: Foto profil: ${fotoProfil.value}');
print('DEBUG WARGA: Foto profil dari roleData: ${fotoProfil.value}');
}
} else {
print('DEBUG WARGA: User bukan warga');
@ -142,10 +152,8 @@ class WargaDashboardController extends GetxController {
print('DEBUG WARGA: userData null');
}
// Cek dan ambil foto profil jika belum ada
if (fotoProfil.isEmpty) {
_fetchProfilePhoto();
}
// Ambil foto profil dari database
_fetchProfilePhoto();
}
// Metode untuk mengambil foto profil
@ -156,12 +164,14 @@ class WargaDashboardController extends GetxController {
final wargaData = await _supabaseService.client
.from('warga')
.select('foto_profil')
.eq('user_id', user!.id)
.single();
.eq('id', user!.id) // Menggunakan id, bukan user_id
.maybeSingle();
if (wargaData != null && wargaData['foto_profil'] != null) {
fotoProfil.value = wargaData['foto_profil'];
print('DEBUG WARGA: Foto profil dari API: ${fotoProfil.value}');
} else {
print('DEBUG WARGA: Foto profil tidak ditemukan atau null');
}
} catch (e) {
print('Error fetching profile photo: $e');
@ -586,4 +596,13 @@ class WargaDashboardController extends GetxController {
isLoading.value = false;
}
}
// Metode untuk refresh data setelah update profil atau kembali ke halaman
Future<void> refreshData() async {
print('DEBUG WARGA: Memulai refresh data...');
await _authController.refreshUserData(); // Refresh data dari server
loadUserData(); // Muat ulang data ke variabel lokal
fetchData(); // Ambil data terkait lainnya
print('DEBUG WARGA: Refresh data selesai');
}
}

View File

@ -29,6 +29,8 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildStatisticSection(),
const SizedBox(height: 24),
_buildPenerimaanSummary(),
const SizedBox(height: 24),
_buildRecentPenerimaan(),
@ -296,6 +298,144 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
);
}
Widget _buildStatisticSection() {
// Data untuk statistik
final totalBantuan = controller.penerimaPenyaluran.length;
final totalDiterima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'DITERIMA')
.length;
final totalBelumMenerima = controller.penerimaPenyaluran
.where((item) => item.statusPenerimaan == 'BELUMMENERIMA')
.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Statistik Bantuan',
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatisticCard(
icon: Icons.check_circle,
color: Colors.green,
title: 'Diterima',
value: totalDiterima.toString(),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatisticCard(
icon: Icons.do_not_disturb,
color: Colors.red,
title: 'Belum Menerima',
value: totalBelumMenerima.toString(),
),
),
],
),
// Progress bar untuk persentase bantuan yang diterima
if (totalBantuan > 0) ...[
const SizedBox(height: 16),
Text(
'Kemajuan Penerimaan Bantuan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: totalDiterima / totalBantuan,
minHeight: 12,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
),
),
const SizedBox(height: 8),
Text(
'${(totalDiterima / totalBantuan * 100).toStringAsFixed(0)}% bantuan telah diterima',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
],
);
}
Widget _buildStatisticCard({
required IconData icon,
required Color color,
required String title,
required String value,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 20,
color: color,
),
),
const Spacer(),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
@ -324,49 +464,93 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
}
}
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [Colors.blue.shade50, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Ringkasan Bantuan',
titleStyle: const TextStyle(
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 16),
if (totalUang > 0)
_buildSummaryItem(
icon: Icons.attach_money,
color: Colors.green,
title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
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',
child: Column(
children: [
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 Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1),
),
...totalNonUang.entries.map((entry) {
return Column(
children: [
_buildSummaryItem(
icon: Icons.inventory_2,
color: Colors.blue,
title: 'Total Bantuan ${entry.key}',
value: '${entry.value} ${entry.key}',
),
if (entry != totalNonUang.entries.last)
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1),
),
],
);
}).toList(),
],
if (totalUang == 0 && totalNonUang.isEmpty)
_buildSummaryItem(
icon: Icons.info_outline,
color: Colors.grey,
title: 'Belum Ada Bantuan',
value: 'Anda belum menerima bantuan',
),
],
),
),
],
),
),
@ -422,53 +606,140 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
Widget _buildRecentPenerimaan() {
if (controller.penerimaPenyaluran.isEmpty) {
return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
SectionHeader(
title: 'Bantuan Terbaru',
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Icon(
Icons.info_outline,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum Ada Bantuan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
Text(
'Data bantuan akan muncul di sini ketika Anda menerima bantuan.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
final maxItems = controller.penerimaPenyaluran.length > 2
? 2
: controller.penerimaPenyaluran.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed(Routes.wargaPenerimaan);
},
),
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'),
),
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
title: 'Bantuan Terbaru',
viewAllText: 'Lihat Semua',
onViewAll: () {
Get.toNamed(Routes.wargaPenerimaan);
},
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
padding: EdgeInsets.zero,
),
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: ElevatedButton.icon(
onPressed: () {
Get.toNamed(Routes.wargaPenerimaan);
},
icon: const Icon(Icons.list),
label: const Text('Lihat Semua Bantuan'),
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
),
),
],
),
);
}
}

View File

@ -328,8 +328,8 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
),
const SizedBox(height: 8),
StatusBadge(
status:
penyaluran.statusPenerimaan ?? 'MENUNGGU',
status: penyaluran.statusPenerimaan ??
'BELUMMENERIMA',
fontSize: 14,
padding: const EdgeInsets.symmetric(
horizontal: 12,
@ -423,7 +423,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
),
const SizedBox(width: 8),
StatusBadge(
status: penyaluran.statusPenerimaan ?? 'MENUNGGU',
status: penyaluran.statusPenerimaan ?? 'BELUMMENERIMA',
fontSize: 12,
padding: const EdgeInsets.symmetric(
horizontal: 8,

View File

@ -4,16 +4,28 @@ import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_con
import 'package:penyaluran_app/app/modules/warga/views/warga_dashboard_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';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class WargaView extends GetView<WargaDashboardController> {
const WargaView({super.key});
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
WargaView({super.key});
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
// Tambahkan listener untuk refresh data saat fokus didapatkan kembali
// misalnya ketika kembali dari halaman profil
WidgetsBinding.instance.addPostFrameCallback((_) {
final focusNode = FocusNode();
FocusScope.of(context).requestFocus(focusNode);
focusNode.addListener(() {
if (focusNode.hasFocus) {
print('DEBUG WARGA: Halaman mendapatkan fokus, memuat ulang data');
controller.refreshData();
}
});
});
return Scaffold(
key: scaffoldKey,
@ -81,40 +93,7 @@ class WargaView extends GetView<WargaDashboardController> {
),
],
),
drawer: Obx(() => AppDrawer(
nama: controller.nama,
role: 'Warga',
desa: controller.desa,
notificationCount: controller.jumlahNotifikasiBelumDibaca.value,
onLogout: controller.logout,
menuItems: [
DrawerMenuItem(
icon: Icons.dashboard_outlined,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () => controller.changeTab(0),
),
DrawerMenuItem(
icon: Icons.volunteer_activism_outlined,
title: 'Penerimaan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () => controller.changeTab(1),
),
DrawerMenuItem(
icon: Icons.report_problem_outlined,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 2,
badgeCount: controller.totalPengaduanProses.value,
badgeColor: Colors.orange,
onTap: () => controller.changeTab(2),
),
DrawerMenuItem(
icon: Icons.description_outlined,
title: 'Laporan Penyaluran',
onTap: () => Get.toNamed('/laporan-penyaluran'),
),
],
)),
drawer: _buildDrawer(context),
body: Obx(() {
switch (controller.activeTabIndex.value) {
case 0:
@ -158,4 +137,276 @@ class WargaView extends GetView<WargaDashboardController> {
)),
);
}
Widget _buildDrawer(BuildContext context) {
// Muat ulang data foto profil ketika drawer dibuka
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.fotoProfil.isEmpty) {
controller.loadUserData();
}
});
return Drawer(
child: Column(
children: [
Container(
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 16,
bottom: 24,
left: 16,
right: 16),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty
? Icon(
Icons.person,
color: Colors.white,
size: 40,
)
: null,
),
),
SizedBox(height: 16),
Text(
'Halo,',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
Text(
controller.nama,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 22,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Warga',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on,
color: Colors.white,
size: 14,
),
SizedBox(width: 4),
Text(
controller.desa ?? 'Tidak ada desa',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
],
),
],
),
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildMenuCategory('Menu Utama'),
Obx(() => _buildMenuItem(
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () {
Navigator.pop(context);
controller.changeTab(0);
},
)),
Obx(() => _buildMenuItem(
icon: Icons.volunteer_activism_outlined,
activeIcon: Icons.volunteer_activism,
title: 'Penerimaan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () {
Navigator.pop(context);
controller.changeTab(1);
},
)),
Obx(() => _buildMenuItem(
icon: Icons.report_problem_outlined,
activeIcon: Icons.report_problem,
title: 'Pengaduan',
isSelected: controller.activeTabIndex.value == 2,
badge: controller.totalPengaduanProses.value > 0
? controller.totalPengaduanProses.value.toString()
: null,
onTap: () {
Navigator.pop(context);
controller.changeTab(2);
},
)),
_buildMenuCategory('Pengaturan'),
_buildMenuItem(
icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () async {
Navigator.pop(context);
await Get.toNamed('/profile');
// Refresh data ketika kembali dari profil
controller.refreshData();
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
onTap: () {
Navigator.pop(context);
controller.logout();
},
isLogout: true,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Widget _buildMenuCategory(String title) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
IconData? activeIcon,
required String title,
bool isSelected = false,
String? badge,
required Function() onTap,
bool isLogout = false,
}) {
return AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: ListTile(
leading: Icon(
isSelected ? (activeIcon ?? icon) : icon,
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
),
title: Text(
title,
style: TextStyle(
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
fontWeight: isSelected ? FontWeight.bold : null,
),
),
trailing: badge != null
? Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
badge,
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
)
: null,
onTap: onTap,
),
);
}
}

View File

@ -57,7 +57,7 @@ class AppPages {
),
GetPage(
name: Routes.wargaDashboard,
page: () => const WargaView(),
page: () => WargaView(),
binding: WargaBinding(),
),
GetPage(
@ -65,7 +65,7 @@ class AppPages {
page: () {
final controller = Get.find<WargaDashboardController>();
controller.activeTabIndex.value = 1;
return const WargaView();
return WargaView();
},
binding: WargaBinding(),
),
@ -74,7 +74,7 @@ class AppPages {
page: () {
final controller = Get.find<WargaDashboardController>();
controller.activeTabIndex.value = 2;
return const WargaView();
return WargaView();
},
binding: WargaBinding(),
),

View File

@ -423,22 +423,39 @@ class SupabaseService extends GetxService {
}
}
Future<int?> getTotalBantuan() async {
// Metode untuk mendapatkan data penerima terbaru
Future<List<Map<String, dynamic>>?> getPenerimaTerbaru() async {
try {
final response = await client.from('stok_bantuan').select('jumlah');
final response = await client
.from('warga')
.select('*')
.eq('status', 'AKTIF')
.order('created_at', ascending: false)
.limit(5);
double total = 0;
for (var item in response) {
total += (item['jumlah'] ?? 0);
}
return total.toInt();
return response;
} catch (e) {
print('Error getting total bantuan: $e');
print('Error getting penerima terbaru: $e');
return null;
}
}
// Future<int?> getTotalBantuan() async {
// try {
// final response = await client.from('stok_bantuan').select('jumlah');
// double total = 0;
// for (var item in response) {
// total += (item['jumlah'] ?? 0);
// }
// return total.toInt();
// } catch (e) {
// print('Error getting total bantuan: $e');
// return null;
// }
// }
Future<int?> getTotalPenyaluran() async {
try {
final response = await client
@ -453,6 +470,18 @@ class SupabaseService extends GetxService {
}
}
// Metode untuk mendapatkan total semua penyaluran (termasuk semua status)
Future<int?> getTotalSemuaPenyaluran() async {
try {
final response = await client.from('penyaluran_bantuan').select('id');
return response.length;
} catch (e) {
print('Error getting total semua penyaluran: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getNotifikasiBelumDibaca(
String userId) async {
try {
@ -484,7 +513,13 @@ class SupabaseService extends GetxService {
final response = await client
.from('penyaluran_bantuan')
.select('*')
.select('''
*,
kategori_bantuan(*),
lokasi_penyaluran:lokasi_penyaluran_id(
id, nama, alamat_lengkap
)
''')
.gte('tanggal_penyaluran', todayUtc)
.lt('tanggal_penyaluran', tomorrowUtc)
.inFilter('status', ['AKTIF', 'DIJADWALKAN']);
@ -713,6 +748,21 @@ class SupabaseService extends GetxService {
}
}
// Metode untuk mendapatkan total penitipan terverifikasi
Future<int?> getTotalPenitipanTerverifikasi() async {
try {
final response = await client
.from('penitipan_bantuan')
.select('id')
.eq('status', 'TERVERIFIKASI');
return response.length;
} catch (e) {
print('Error getting total penitipan terverifikasi: $e');
return null;
}
}
// Metode untuk mengambil data penitipan bantuan dengan status TERVERIFIKASI
Future<List<Map<String, dynamic>>?> getPenitipanBantuanTerverifikasi() async {
try {
@ -1109,11 +1159,31 @@ class SupabaseService extends GetxService {
Future<List<Map<String, dynamic>>?> getPenyaluranBantuanByWargaId(
String wargaId) async {
try {
final response = await client
.from('penyaluran_bantuan')
.select('*, stok_bantuan:stok_bantuan_id(*)')
.eq('penerima_id', wargaId)
.order('tanggal_penyaluran', ascending: false);
// Pertama, cari warga berdasarkan NIP untuk mendapatkan UUID-nya
final wargaResponse = await client
.from('warga')
.select('id')
.eq('id', wargaId)
.maybeSingle();
if (wargaResponse == null) {
print('Warning: Warga dengan NIP $wargaId tidak ditemukan');
return [];
}
final wargaUuid = wargaResponse['id'];
// Kemudian gunakan UUID untuk mencari penyaluran bantuan
final response = await client.from('penerima_penyaluran').select('''
*,
penyaluran_bantuan:penyaluran_bantuan_id(
*,
kategori_bantuan(*),
lokasi_penyaluran:lokasi_penyaluran_id(
id, nama, alamat_lengkap
)
)
''').eq('warga_id', wargaUuid).order('created_at', ascending: false);
return response;
} catch (e) {
@ -1339,6 +1409,53 @@ class SupabaseService extends GetxService {
}
}
// Metode untuk memperbarui profil donatur
Future<void> updateDonaturProfile({
required String userId,
required String nama,
required String email,
String? noHp,
String? fotoProfil,
}) async {
try {
// Buat map untuk update data
final Map<String, dynamic> updateData = {
'nama': nama,
'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap
'no_hp': noHp,
'updated_at': DateTime.now().toIso8601String(),
};
// Tambahkan foto profil jika ada
if (fotoProfil != null) {
// Jika string kosong, set null untuk menghapus foto profil
if (fotoProfil.isEmpty) {
updateData['foto_profil'] = null;
} else {
updateData['foto_profil'] = fotoProfil;
}
}
// Update data donatur
await client.from('donatur').update(updateData).eq('id', userId);
// Update email di auth.users jika berubah
if (email != client.auth.currentUser?.email) {
// Gunakan metode updateUserEmail
await client.auth.updateUser(UserAttributes(
email: email,
));
}
// Hapus cache user profile
_cachedUserProfile = null;
print('Cache profil user dihapus setelah update donatur');
} catch (e) {
print('Error updating donatur profile: $e');
throw e.toString();
}
}
// Metode untuk membuat profil petugas desa
Future<void> createPetugasDesaProfile({
required String nama_lengkap,
@ -1647,7 +1764,12 @@ class SupabaseService extends GetxService {
// Tambahkan foto profil jika ada
if (fotoProfil != null) {
updateData['foto_profil'] = fotoProfil;
// Jika string kosong, set null untuk menghapus foto profil
if (fotoProfil.isEmpty) {
updateData['foto_profil'] = null;
} else {
updateData['foto_profil'] = fotoProfil;
}
}
// Update data warga
@ -1660,50 +1782,16 @@ class SupabaseService extends GetxService {
email: email,
));
}
// Hapus cache user profile
_cachedUserProfile = null;
print('Cache profil user dihapus setelah update warga');
} catch (e) {
print('Error updating warga profile: $e');
throw e.toString();
}
}
// Metode untuk memperbarui profil donatur
Future<void> updateDonaturProfile({
required String userId,
required String nama,
required String email,
String? noHp,
String? fotoProfil,
}) async {
try {
// Buat map untuk update data
final Map<String, dynamic> updateData = {
'nama': nama,
'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap
'no_hp': noHp,
'updated_at': DateTime.now().toIso8601String(),
};
// Tambahkan foto profil jika ada
if (fotoProfil != null) {
updateData['foto_profil'] = fotoProfil;
}
// Update data donatur
await client.from('donatur').update(updateData).eq('id', userId);
// Update email di auth.users jika berubah
if (email != client.auth.currentUser?.email) {
// Gunakan metode updateUserEmail
await client.auth.updateUser(UserAttributes(
email: email,
));
}
} catch (e) {
print('Error updating donatur profile: $e');
throw e.toString();
}
}
// Metode untuk memperbarui profil petugas desa
Future<void> updatePetugasDesaProfile({
required String userId,
@ -1722,7 +1810,12 @@ class SupabaseService extends GetxService {
// Tambahkan foto profil jika ada
if (fotoProfil != null) {
updateData['foto_profil'] = fotoProfil;
// Jika string kosong, set null untuk menghapus foto profil
if (fotoProfil.isEmpty) {
updateData['foto_profil'] = null;
} else {
updateData['foto_profil'] = fotoProfil;
}
}
// Update data petugas desa
@ -1735,6 +1828,10 @@ class SupabaseService extends GetxService {
email: email,
));
}
// Hapus cache user profile
_cachedUserProfile = null;
print('Cache profil user dihapus setelah update petugas desa');
} catch (e) {
print('Error updating petugas desa profile: $e');
throw e.toString();

View File

@ -105,7 +105,7 @@ class BantuanCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
StatusBadge(
status: item.statusPenerimaan ?? 'MENUNGGU',
status: item.statusPenerimaan ?? 'BELUMMENERIMA',
fontSize: 10,
padding: const EdgeInsets.symmetric(
horizontal: 8,
@ -177,7 +177,7 @@ class BantuanCard extends StatelessWidget {
children: [
StatusBadge(status: item.statusPenyaluran ?? ""),
StatusBadge(
status: item.statusPenerimaan ?? 'MENUNGGU',
status: item.statusPenerimaan ?? 'BELUMMENERIMA',
),
],
),

View File

@ -693,6 +693,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.11.3"
percent_indicator:
dependency: "direct main"
description:
name: percent_indicator
sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
petitparser:
dependency: transitive
description:

View File

@ -90,6 +90,7 @@ dependencies:
path_provider: ^2.1.2
# Package untuk membuka file
open_file: ^3.3.2
percent_indicator: ^4.2.4
dev_dependencies:
flutter_test: