Perbarui model dan tampilan untuk menambahkan properti fotoProfil di DonaturModel, PetugasDesaModel, dan WargaModel. Modifikasi controller dan tampilan untuk mendukung pengambilan dan penampilan foto profil pengguna. Tambahkan fungsionalitas baru untuk menampilkan foto profil di berbagai tampilan, termasuk detail penerima dan dashboard warga. Perbarui rute aplikasi untuk mencakup halaman profil pengguna.

This commit is contained in:
Khafidh Fuadi
2025-03-25 12:21:37 +07:00
parent 8e9553d1fc
commit 32736be867
19 changed files with 2138 additions and 752 deletions

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD> ?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08>ّ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
 
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt  <08><EFBFBD><EFBFBD><EFBFBD><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>2  <20><><EFBFBD><EFBFBD><EFBFBD>2~
| |
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json  <08><EFBFBD><EFBFBD><EFBFBD><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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD> <EFBFBD>
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json  <08><EFBFBD><EFBFBD><EFBFBD><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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
n n
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 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
r r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2y pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt  <08>ّ<EFBFBD><EFBFBD>2y
w w
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 uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt  <08>ّ<EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2z 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><D991>2 ~ vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json  <08>ّ<EFBFBD><D991>2 ~
| |
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  <08>ّ<EFBFBD><D991>2 zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin  <08>ّ<EFBFBD><D991>2
<EFBFBD> <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><D991>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2w <EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt  <08>ّ<EFBFBD><D991>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><D991>2 sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json  <08>ّ<EFBFBD><D991>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2|  ( <20><><EFBFBD><EFBFBD><EFBFBD>2|

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD> ?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08>ݑ<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\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\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\android_gradle_build.json  <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\armeabi-v7a\android_gradle_build.json  <08>ݑ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
<EFBFBD> <EFBFBD>
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json  <08><EFBFBD><EFBFBD><EFBFBD><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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
p p
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 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
t t
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{ rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt  <08>ݑ<EFBFBD><EFBFBD>2{
y y
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 wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt  <08>ޑ<EFBFBD><EFBFBD>2
K <20><><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><DE91>2 <09> xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json  <08>ޑ<EFBFBD><DE91>2 <09>
~ ~
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin  <08>ޑ<EFBFBD><DE91>2 |D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin  <08>ޑ<EFBFBD><DE91>2
<EFBFBD> <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><DF91>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2y <EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt  <08>ߑ<EFBFBD><DF91>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><DF91>2 uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json  <08>ߑ<EFBFBD><DF91>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2~  ( <20><><EFBFBD><EFBFBD><EFBFBD>2~

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{ ?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
y y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2x wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2x
v v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2} tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
{ {
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json  <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
h h
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  <08><><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja  <08><><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
l l
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2s jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2s
q q
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2t K <20><><EFBFBD><EFBFBD><EFBFBD>2t
r r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 x pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 x
v v
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  <08><><EFBFBD><EFBFBD><EFBFBD>2 tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin  <08><><EFBFBD><EFBFBD><EFBFBD>2
~ ~
| |
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2q zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt  <08><><EFBFBD><EFBFBD><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
o o
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json  <08><><EFBFBD><EFBFBD><EFBFBD>2 mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json  <08><><EFBFBD><EFBFBD><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2v  ( <20><><EFBFBD><EFBFBD><EFBFBD>2v

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A 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 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 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 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 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 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 { 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 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> <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 }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 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

@ -9,6 +9,7 @@ class DonaturModel {
final String? jenis; final String? jenis;
final String? deskripsi; final String? deskripsi;
final String? status; final String? status;
final String? fotoProfil;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
@ -21,6 +22,7 @@ class DonaturModel {
this.jenis, this.jenis,
this.deskripsi, this.deskripsi,
this.status = 'AKTIF', this.status = 'AKTIF',
this.fotoProfil,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -39,6 +41,7 @@ class DonaturModel {
jenis: json["jenis"], jenis: json["jenis"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
status: json["status"] ?? 'AKTIF', status: json["status"] ?? 'AKTIF',
fotoProfil: json["foto_profil"],
createdAt: json["created_at"] != null createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"]) ? DateTime.parse(json["created_at"])
: null, : null,
@ -56,6 +59,7 @@ class DonaturModel {
"jenis": jenis, "jenis": jenis,
"deskripsi": deskripsi, "deskripsi": deskripsi,
"status": status ?? 'AKTIF', "status": status ?? 'AKTIF',
"foto_profil": fotoProfil,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };

View File

@ -10,6 +10,7 @@ class PetugasDesaModel {
final String? email; final String? email;
final String? jabatan; final String? jabatan;
final String? nip; final String? nip;
final String? fotoProfil;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final DesaModel? desa; final DesaModel? desa;
@ -23,6 +24,7 @@ class PetugasDesaModel {
this.email, this.email,
this.jabatan, this.jabatan,
this.nip, this.nip,
this.fotoProfil,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.desa, this.desa,
@ -48,6 +50,7 @@ class PetugasDesaModel {
email: json["email"], email: json["email"],
jabatan: json["jabatan"], jabatan: json["jabatan"],
nip: json["nip"], nip: json["nip"],
fotoProfil: json["foto_profil"],
createdAt: json["created_at"] != null createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"]) ? DateTime.parse(json["created_at"])
: null, : null,
@ -67,6 +70,7 @@ class PetugasDesaModel {
"email": email, "email": email,
"jabatan": jabatan, "jabatan": jabatan,
"nip": nip, "nip": nip,
"foto_profil": fotoProfil,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };

View File

@ -17,6 +17,7 @@ class WargaModel {
final String? kategoriEkonomi; final String? kategoriEkonomi;
final String? status; final String? status;
final String? catatan; final String? catatan;
final String? fotoProfil;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final DesaModel? desa; final DesaModel? desa;
@ -36,6 +37,7 @@ class WargaModel {
this.kategoriEkonomi, this.kategoriEkonomi,
this.status = 'AKTIF', this.status = 'AKTIF',
this.catatan, this.catatan,
this.fotoProfil,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.desa, this.desa,
@ -69,6 +71,7 @@ class WargaModel {
kategoriEkonomi: json["kategori_ekonomi"], kategoriEkonomi: json["kategori_ekonomi"],
status: json["status"] ?? 'AKTIF', status: json["status"] ?? 'AKTIF',
catatan: json["catatan"], catatan: json["catatan"],
fotoProfil: json["foto_profil"],
createdAt: json["created_at"] != null createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"]) ? DateTime.parse(json["created_at"])
: null, : null,
@ -94,6 +97,7 @@ class WargaModel {
"kategori_ekonomi": kategoriEkonomi, "kategori_ekonomi": kategoriEkonomi,
"status": status, "status": status,
"catatan": catatan, "catatan": catatan,
"foto_profil": fotoProfil,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };

View File

@ -1,5 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart'; import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class PenerimaController extends GetxController { class PenerimaController extends GetxController {
@ -7,6 +8,11 @@ class PenerimaController extends GetxController {
<Map<String, dynamic>>[].obs; <Map<String, dynamic>>[].obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
// Variabel untuk menyimpan daftar penyaluran bantuan untuk penerima tertentu
final RxList<Map<String, dynamic>> daftarPenyaluran =
<Map<String, dynamic>>[].obs;
final RxBool isLoadingPenyaluran = false.obs;
// Variabel untuk halaman konfirmasi penerima // Variabel untuk halaman konfirmasi penerima
final RxBool isKonfirmasiChecked = false.obs; final RxBool isKonfirmasiChecked = false.obs;
final RxBool isIdentitasChecked = false.obs; final RxBool isIdentitasChecked = false.obs;
@ -37,9 +43,30 @@ class PenerimaController extends GetxController {
super.onClose(); super.onClose();
} }
void fetchDaftarPenerima() { void fetchDaftarPenerima() async {
isLoading.value = true; isLoading.value = true;
try {
// Get data penerima dari database
final penerimaBantuan = await SupabaseService.to.getPenerimaBantuan();
if (penerimaBantuan != null) {
daftarPenerima.value = penerimaBantuan;
} else {
// Gunakan data dummy jika gagal mendapatkan data dari database
_loadDummyData();
}
} catch (e) {
print('Error fetching penerima: $e');
// Gunakan data dummy sebagai fallback
_loadDummyData();
} finally {
isLoading.value = false;
}
}
// Metode untuk memuat data dummy
void _loadDummyData() {
// Simulasi data penerima // Simulasi data penerima
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
daftarPenerima.value = [ daftarPenerima.value = [
@ -134,7 +161,6 @@ class PenerimaController extends GetxController {
'terverifikasi': true, 'terverifikasi': true,
}, },
]; ];
isLoading.value = false;
}); });
} }
@ -167,6 +193,79 @@ class PenerimaController extends GetxController {
} }
} }
// Metode untuk mengambil daftar penyaluran bantuan berdasarkan ID warga
Future<void> fetchPenyaluranByWargaId(String wargaId) async {
isLoadingPenyaluran.value = true;
daftarPenyaluran.clear();
try {
final penyaluranBantuan =
await SupabaseService.to.getPenyaluranBantuanByWargaId(wargaId);
if (penyaluranBantuan != null && penyaluranBantuan.isNotEmpty) {
daftarPenyaluran.value = penyaluranBantuan;
} else {
// Gunakan data dummy jika tidak ada data dari database
_loadDummyPenyaluran(wargaId);
}
} catch (e) {
print('Error fetching penyaluran bantuan: $e');
// Gunakan data dummy sebagai fallback
_loadDummyPenyaluran(wargaId);
} finally {
isLoadingPenyaluran.value = false;
}
}
// Metode untuk memuat data dummy penyaluran
void _loadDummyPenyaluran(String wargaId) {
// Data dummy penyaluran bantuan (hanya untuk demo)
daftarPenyaluran.value = [
{
'id': '1',
'penerima_id': wargaId,
'tanggal_penyaluran':
DateTime.now().subtract(const Duration(days: 5)).toIso8601String(),
'status': 'TERLAKSANA',
'stok_bantuan': {
'nama': 'Paket Sembako',
'jenis': 'Bahan Pokok',
'kuantitas': '1 Paket',
},
'keterangan': 'Bantuan pangan rutin bulanan',
'bukti_penyaluran': 'assets/images/bukti_penyaluran.jpg',
},
{
'id': '2',
'penerima_id': wargaId,
'tanggal_penyaluran':
DateTime.now().subtract(const Duration(days: 35)).toIso8601String(),
'status': 'TERLAKSANA',
'stok_bantuan': {
'nama': 'Bantuan Pendidikan',
'jenis': 'Alat Tulis',
'kuantitas': '1 Paket',
},
'keterangan': 'Bantuan sekolah semester baru',
'bukti_penyaluran': 'assets/images/bukti_penyaluran.jpg',
},
{
'id': '3',
'penerima_id': wargaId,
'tanggal_penyaluran':
DateTime.now().add(const Duration(days: 2)).toIso8601String(),
'status': 'DIJADWALKAN',
'stok_bantuan': {
'nama': 'Paket Sembako',
'jenis': 'Bahan Pokok',
'kuantitas': '1 Paket',
},
'keterangan': 'Bantuan pangan rutin bulanan',
'bukti_penyaluran': null,
},
];
}
// Fungsi untuk memilih tanggal penyaluran // Fungsi untuk memilih tanggal penyaluran
Future<void> pilihTanggalPenyaluran(BuildContext context) async { Future<void> pilihTanggalPenyaluran(BuildContext context) async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(

View File

@ -27,6 +27,9 @@ class PetugasDesaController extends GetxController {
// Data profil pengguna dari cache // Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({}); final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
// Variabel untuk foto profil
final RxString fotoProfil = ''.obs;
// Model desa dari cache // Model desa dari cache
final Rx<DesaModel?> desaModel = Rx<DesaModel?>(null); final Rx<DesaModel?> desaModel = Rx<DesaModel?>(null);
@ -102,6 +105,33 @@ class PetugasDesaController extends GetxController {
return 'Petugas Desa'; return 'Petugas Desa';
} }
// Getter untuk foto profil
String? get profilePhotoUrl {
// 1. Coba ambil dari fotoProfil yang sudah disimpan
if (fotoProfil.isNotEmpty) {
return fotoProfil.value;
}
// 2. Coba ambil dari roleData jika merupakan PetugasDesaModel
final userData = _authController.userData;
if (userData != null && userData.roleData is PetugasDesaModel) {
final petugasData = userData.roleData as PetugasDesaModel;
if (petugasData.fotoProfil != null &&
petugasData.fotoProfil!.isNotEmpty) {
return petugasData.fotoProfil;
}
}
// 3. Coba ambil dari role_data di userProfile
if (userProfile['role_data'] != null &&
userProfile['role_data'] is Map<String, dynamic> &&
userProfile['role_data']['foto_profil'] != null) {
return userProfile['role_data']['foto_profil'];
}
return null;
}
// Getter untuk counter dari CounterService // Getter untuk counter dari CounterService
RxInt get jumlahNotifikasiBelumDibaca => RxInt get jumlahNotifikasiBelumDibaca =>
_counterService.jumlahNotifikasiBelumDibaca; _counterService.jumlahNotifikasiBelumDibaca;
@ -212,6 +242,14 @@ class PetugasDesaController extends GetxController {
'desa': petugasData.desa?.toJson(), 'desa': petugasData.desa?.toJson(),
}; };
// Ambil foto profil jika ada
if (petugasData.fotoProfil != null &&
petugasData.fotoProfil!.isNotEmpty) {
fotoProfil.value = petugasData.fotoProfil!;
print(
'DEBUG: Foto profil dari petugasData: ${fotoProfil.value}');
}
return; // Data sudah lengkap, tidak perlu fetch lagi return; // Data sudah lengkap, tidak perlu fetch lagi
} }
} }
@ -223,6 +261,14 @@ class PetugasDesaController extends GetxController {
if (baseProfile != null) { if (baseProfile != null) {
userProfile.value = baseProfile; userProfile.value = baseProfile;
// Cek dan ambil foto profil
if (baseProfile['role_data'] != null &&
baseProfile['role_data'] is Map<String, dynamic> &&
baseProfile['role_data']['foto_profil'] != null) {
fotoProfil.value = baseProfile['role_data']['foto_profil'];
print('DEBUG: Foto profil dari API: ${fotoProfil.value}');
}
if (baseProfile['desa'] != null && if (baseProfile['desa'] != null &&
baseProfile['desa'] is Map<String, dynamic>) { baseProfile['desa'] is Map<String, dynamic>) {
try { try {
@ -594,7 +640,9 @@ class PetugasDesaController extends GetxController {
// Metode untuk mengubah tab aktif // Metode untuk mengubah tab aktif
void changeTab(int index) { void changeTab(int index) {
print('Mengubah tab ke index: $index (dari: ${activeTabIndex.value})');
activeTabIndex.value = index; activeTabIndex.value = index;
print('activeTabIndex sekarang: ${activeTabIndex.value}');
// Jika tab penitipan dipilih, muat ulang data penitipan // Jika tab penitipan dipilih, muat ulang data penitipan
if (index == 2) { if (index == 2) {
@ -621,6 +669,8 @@ class PetugasDesaController extends GetxController {
print('Error saat memanggil onTabReactivated: $e'); print('Error saat memanggil onTabReactivated: $e');
} }
} }
// Paksa update UI
activeTabIndex.refresh();
} }
// Metode untuk logout // Metode untuk logout

View File

@ -161,47 +161,78 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
Widget _buildPenerimaCard( Widget _buildPenerimaCard(
BuildContext context, Map<String, dynamic> penerima) { BuildContext context, Map<String, dynamic> penerima) {
return Card( final statusActive = penerima['status'] == 'AKTIF';
return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
elevation: 2, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
), ),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// Navigasi ke halaman detail penerima // Navigasi ke halaman detail penerima
Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']); Get.toNamed('/daftar-penerima/detail', arguments: penerima['id']);
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
child: Padding( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
AppTheme.primaryColor.withOpacity(0.03),
],
),
),
child: Row( child: Row(
children: [ children: [
// Foto profil // Foto profil dengan animasi hero
CircleAvatar( Hero(
radius: 30, tag: 'penerima-${penerima['id']}',
backgroundColor: AppTheme.primaryColor.withOpacity(0.1), child: Container(
child: penerima['foto'] != null decoration: BoxDecoration(
? ClipRRect( shape: BoxShape.circle,
borderRadius: BorderRadius.circular(30), border: Border.all(
child: Image.asset( color: AppTheme.primaryColor.withOpacity(0.3),
penerima['foto'], width: 2),
width: 60, boxShadow: [
height: 60, BoxShadow(
fit: BoxFit.cover, color: AppTheme.primaryColor.withOpacity(0.1),
errorBuilder: (context, error, stackTrace) { blurRadius: 8,
return const Icon( offset: const Offset(0, 3),
Icons.person,
size: 30,
color: AppTheme.primaryColor,
);
},
), ),
) ],
: const Icon( ),
child: CircleAvatar(
radius: 35,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? Icon(
Icons.person, Icons.person,
size: 30, size: 35,
color: AppTheme.primaryColor, color: AppTheme.primaryColor.withOpacity(0.7),
)
: null,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -214,9 +245,9 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
penerima['nama'] ?? '', penerima['nama_lengkap'] ?? 'Tanpa Nama',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
maxLines: 1, maxLines: 1,
@ -224,106 +255,107 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
), ),
), ),
if (penerima['terverifikasi'] == true) if (penerima['terverifikasi'] == true)
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.verified,
color: Colors.green,
size: 18,
),
),
],
),
const SizedBox(height: 8),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1), color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade300,
width: 1,
),
),
child: Text(
'NIK: ${penerima['nik'] ?? 'Belum Ada'}',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusActive
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: statusActive
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
width: 1,
),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon( Icon(
Icons.verified, statusActive
size: 14, ? Icons.check_circle
color: Colors.green, : Icons.cancel,
size: 12,
color:
statusActive ? Colors.green : Colors.red,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
const Text(
'Terverifikasi',
style: TextStyle(
fontSize: 12,
color: Colors.green,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text( Text(
'NIK: ${penerima['nik'] ?? ''}', statusActive ? 'Aktif' : 'Tidak Aktif',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
_buildStatusBadge(penerima['status']),
// const SizedBox(height: 8),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// penerima['alamatLengkap'] ?? '',
// style: TextStyle(
// fontSize: 12,
// color: Colors.grey[600],
// ),
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// _buildStatusBadge(penerima['status']),
// ],
// ),
],
),
),
],
),
),
),
);
}
Widget _buildStatusBadge(String? status) {
Color backgroundColor;
Color textColor;
switch (status) {
case 'Selesai':
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green;
break;
case 'Terjadwal':
backgroundColor = Colors.blue.withOpacity(0.1);
textColor = Colors.blue;
break;
case 'Belum disalurkan':
backgroundColor = Colors.orange.withOpacity(0.1);
textColor = Colors.orange;
break;
default:
backgroundColor = Colors.grey.withOpacity(0.1);
textColor = Colors.grey;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
status ?? 'Tidak diketahui',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: textColor, fontWeight: FontWeight.w500,
color: statusActive
? Colors.green
: Colors.red,
),
),
],
),
),
const Spacer(),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: AppTheme.primaryColor,
),
),
],
),
],
),
),
],
),
),
), ),
), ),
); );
@ -369,7 +401,7 @@ class PenerimaSearchDelegate extends SearchDelegate {
Widget _buildSearchResults() { Widget _buildSearchResults() {
final filteredList = daftarPenerima.where((penerima) { final filteredList = daftarPenerima.where((penerima) {
final nama = penerima['nama']?.toString().toLowerCase() ?? ''; final nama = penerima['nama_lengkap']?.toString().toLowerCase() ?? '';
final nik = penerima['nik']?.toString().toLowerCase() ?? ''; final nik = penerima['nik']?.toString().toLowerCase() ?? '';
final alamat = penerima['alamatLengkap']?.toString().toLowerCase() ?? ''; final alamat = penerima['alamatLengkap']?.toString().toLowerCase() ?? '';
final searchLower = query.toLowerCase(); final searchLower = query.toLowerCase();
@ -403,13 +435,18 @@ class PenerimaSearchDelegate extends SearchDelegate {
}, },
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1), backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: const Icon( backgroundImage: penerima['foto_profil'] != null
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? const Icon(
Icons.person, Icons.person,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), )
: null,
), ),
title: Text( title: Text(
penerima['nama'] ?? '', penerima['nama_lengkap'] ?? '',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),

View File

@ -79,12 +79,263 @@ class DetailDonaturView extends GetView<DonaturController> {
final totalNilaiDonasiUangFormatted = final totalNilaiDonasiUangFormatted =
controller.formatRupiah(totalNilaiDonasiUang); controller.formatRupiah(totalNilaiDonasiUang);
// Pilih warna berdasarkan jenis donatur
Color jenisColor = donatur.jenis == 'Perusahaan'
? Colors.blue
: donatur.jenis == 'Organisasi'
? Colors.green
: Colors.orange;
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header dengan informasi utama donatur // Header dengan informasi utama donatur - desain yang lebih menarik
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: jenisColor.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Card(
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
jenisColor.withOpacity(0.05),
],
),
),
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Avatar dan nama donatur dengan layout yang lebih baik
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: jenisColor.withOpacity(0.7), width: 2),
boxShadow: [
BoxShadow(
color: jenisColor.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Hero(
tag: 'donatur-${donatur.id}',
child: CircleAvatar(
radius: 45,
backgroundColor: jenisColor.withOpacity(0.1),
backgroundImage: donatur.fotoProfil != null &&
donatur.fotoProfil!.isNotEmpty
? NetworkImage(donatur.fotoProfil!)
: null,
child: (donatur.fotoProfil == null ||
donatur.fotoProfil!.isEmpty)
? Icon(
jenisIcon,
size: 45,
color: jenisColor.withOpacity(0.8),
)
: null,
),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
donatur.nama ?? 'Tanpa Nama',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: jenisColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: jenisColor.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
jenisIcon,
size: 16,
color: jenisColor,
),
const SizedBox(width: 6),
Text(
donatur.jenis ?? 'Tidak Diketahui',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: jenisColor,
),
),
],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: donatur.status == 'AKTIF'
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: donatur.status == 'AKTIF'
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
donatur.status == 'AKTIF'
? Icons.check_circle
: Icons.cancel,
size: 16,
color: donatur.status == 'AKTIF'
? Colors.green
: Colors.red,
),
const SizedBox(width: 6),
Text(
donatur.status == 'AKTIF'
? 'Aktif'
: 'Tidak Aktif',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: donatur.status == 'AKTIF'
? Colors.green
: Colors.red,
),
),
],
),
),
],
),
),
],
),
const SizedBox(height: 24),
// Summary cards for donations
Row(
children: [
Expanded(
child: _buildSummaryCard(
title: 'Total Donasi',
value: jumlahDonasi.toString(),
icon: Icons.volunteer_activism,
color: jenisColor,
),
),
const SizedBox(width: 10),
Expanded(
child: _buildSummaryCard(
title: 'Donasi Uang',
value: jumlahDonasiUang.toString(),
icon: Icons.attach_money,
color: jenisColor,
),
),
const SizedBox(width: 10),
Expanded(
child: _buildSummaryCard(
title: 'Donasi Barang',
value: jumlahDonasiBarang.toString(),
icon: Icons.inventory_2,
color: jenisColor,
),
),
],
),
const SizedBox(height: 16),
// Total nilai donasi
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: jenisColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: jenisColor.withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Total Nilai Donasi Uang',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 4),
Text(
totalNilaiDonasiUangFormatted,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: jenisColor,
),
),
],
),
),
],
),
),
),
),
const SizedBox(height: 16),
// Informasi kontak
Card( Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -94,104 +345,6 @@ class DetailDonaturView extends GetView<DonaturController> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
// Avatar dan nama donatur
Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: Icon(
jenisIcon,
size: 40,
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
donatur.nama ?? 'Tanpa Nama',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: donatur.jenis == 'Perusahaan'
? Colors.blue.withOpacity(0.1)
: donatur.jenis == 'Organisasi'
? Colors.green.withOpacity(0.1)
: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
donatur.jenis ?? 'Tidak Diketahui',
style: TextStyle(
fontSize: 12,
color: donatur.jenis == 'Perusahaan'
? Colors.blue
: donatur.jenis == 'Organisasi'
? Colors.green
: Colors.orange,
),
),
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: donatur.status == 'AKTIF'
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
donatur.status == 'AKTIF'
? Icons.check_circle
: Icons.cancel,
size: 12,
color: donatur.status == 'AKTIF'
? Colors.green
: Colors.red,
),
const SizedBox(width: 4),
Text(
donatur.status ?? 'TIDAK AKTIF',
style: TextStyle(
fontSize: 12,
color: donatur.status == 'AKTIF'
? Colors.green
: Colors.red,
),
),
],
),
),
],
),
],
),
),
],
),
const SizedBox(height: 16),
// Informasi kontak
const Divider(), const Divider(),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildInfoItem(Icons.location_on, 'Alamat', _buildInfoItem(Icons.location_on, 'Alamat',
@ -215,85 +368,6 @@ class DetailDonaturView extends GetView<DonaturController> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Ringkasan donasi
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ringkasan Donasi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
'Total Donasi',
'$jumlahDonasi',
Icons.volunteer_activism,
Colors.blue,
),
),
Expanded(
child: _buildStatItem(
'Donasi Uang',
'$jumlahDonasiUang',
Icons.monetization_on,
Colors.green,
),
),
Expanded(
child: _buildStatItem(
'Donasi Barang',
'$jumlahDonasiBarang',
Icons.inventory_2,
Colors.orange,
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Row(
children: [
const Icon(
Icons.monetization_on,
color: Colors.green,
),
const SizedBox(width: 8),
const Text(
'Total Nilai Donasi Uang:',
style: TextStyle(
fontSize: 16,
),
),
const Spacer(),
Text(
totalNilaiDonasiUangFormatted,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Riwayat donasi // Riwayat donasi
Card( Card(
elevation: 2, elevation: 2,
@ -315,12 +389,6 @@ class DetailDonaturView extends GetView<DonaturController> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
TextButton(
onPressed: () {
// Navigasi ke halaman riwayat donasi lengkap
},
child: const Text('Lihat Semua'),
),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -368,19 +436,41 @@ class DetailDonaturView extends GetView<DonaturController> {
); );
} }
Widget _buildStatItem( Widget _buildSummaryCard({
String label, String value, IconData icon, Color color) { required String title,
return Column( required String value,
children: [ required IconData icon,
CircleAvatar( required Color color,
radius: 25, }) {
backgroundColor: color.withOpacity(0.1), return Container(
child: Icon( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
icon, decoration: BoxDecoration(
color: color, color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
), ),
), ),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 24,
color: color.withOpacity(0.7),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text( Text(
value, value,
style: TextStyle( style: TextStyle(
@ -389,16 +479,8 @@ class DetailDonaturView extends GetView<DonaturController> {
color: color, color: color,
), ),
), ),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
], ],
),
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:intl/intl.dart';
class DetailPenerimaView extends GetView<PenerimaController> { class DetailPenerimaView extends GetView<PenerimaController> {
const DetailPenerimaView({super.key}); const DetailPenerimaView({super.key});
@ -10,6 +11,11 @@ class DetailPenerimaView extends GetView<PenerimaController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String id = Get.arguments as String; final String id = Get.arguments as String;
// Panggil metode untuk mengambil data penyaluran saat halaman dibuat
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchPenyaluranByWargaId(id);
});
return Obx(() { return Obx(() {
if (controller.isLoading.value) { if (controller.isLoading.value) {
return Scaffold( return Scaffold(
@ -51,6 +57,9 @@ class DetailPenerimaView extends GetView<PenerimaController> {
// Status penyaluran // Status penyaluran
_buildStatusSection(penerima), _buildStatusSection(penerima),
// Riwayat Penyaluran Bantuan
_buildRiwayatPenyaluran(),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
), ),
@ -63,65 +72,100 @@ class DetailPenerimaView extends GetView<PenerimaController> {
Widget _buildHeader(Map<String, dynamic> penerima) { Widget _buildHeader(Map<String, dynamic> penerima) {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.only(top: 24, bottom: 30, left: 16, right: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppTheme.primaryGradient, gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor.withOpacity(0.8),
AppTheme.primaryColor,
],
),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
), ),
child: Column( child: Column(
children: [ children: [
// Foto profil // Foto profil dengan efek bayangan dan border
CircleAvatar( Container(
radius: 50, decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Hero(
tag: 'penerima-${penerima['id']}',
child: CircleAvatar(
radius: 60,
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: penerima['foto'] != null backgroundImage: penerima['foto_profil'] != null
? ClipRRect( ? NetworkImage(penerima['foto_profil'])
borderRadius: BorderRadius.circular(50), : null,
child: Image.asset( child: penerima['foto_profil'] == null
penerima['foto'], ? Icon(
width: 100,
height: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.person, Icons.person,
size: 50, size: 60,
color: AppTheme.primaryColor, color: AppTheme.primaryColor.withOpacity(0.7),
);
},
),
) )
: const Icon( : null,
Icons.person,
size: 50,
color: AppTheme.primaryColor,
), ),
), ),
const SizedBox(height: 16), ),
const SizedBox(height: 20),
// Nama penerima // Nama penerima dengan stroke effect
Text( Text(
penerima['nama'] ?? '', penerima['nama'] ?? '',
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
shadows: [
Shadow(
blurRadius: 5.0,
color: Colors.black26,
offset: Offset(0, 2),
), ),
],
),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// NIK // NIK dengan style yang lebih menarik
Text( Container(
penerima['nik'] ?? '', padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'NIK: ${penerima['nik'] ?? 'Belum terdaftar'}',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.9),
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Badge terverifikasi // Badge terverifikasi dengan animasi
if (penerima['terverifikasi'] == true) if (penerima['terverifikasi'] == true)
Container( AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 8, vertical: 8,
@ -129,13 +173,20 @@ class DetailPenerimaView extends GetView<PenerimaController> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.successColor, color: AppTheme.successColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.successColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
Icons.verified, Icons.verified,
size: 16, size: 18,
color: Colors.white, color: Colors.white,
), ),
SizedBox(width: 8), SizedBox(width: 8),
@ -143,12 +194,35 @@ class DetailPenerimaView extends GetView<PenerimaController> {
'Terverifikasi', 'Terverifikasi',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
), ),
), ),
], ],
), ),
), ),
// Informasi status aktif
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: penerima['status'] == 'AKTIF'
? Colors.green.withOpacity(0.2)
: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
penerima['status'] == 'AKTIF' ? 'Aktif' : 'Tidak Aktif',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: penerima['status'] == 'AKTIF'
? Colors.white
: Colors.white.withOpacity(0.9),
),
),
),
], ],
), ),
); );
@ -345,4 +419,252 @@ class DetailPenerimaView extends GetView<PenerimaController> {
), ),
); );
} }
// Widget untuk menampilkan riwayat penyaluran bantuan
Widget _buildRiwayatPenyaluran() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Riwayat Penyaluran Bantuan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
if (controller.isLoadingPenyaluran.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.daftarPenyaluran.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Icon(
Icons.history_rounded,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Belum ada riwayat penyaluran bantuan',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
);
}
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.daftarPenyaluran.length,
itemBuilder: (context, index) {
final penyaluran = controller.daftarPenyaluran[index];
return _buildPenyaluranItem(penyaluran);
},
);
}),
],
),
);
}
// 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);
final Color statusColor = penyaluran['status'] == 'TERLAKSANA'
? AppTheme.completedColor
: penyaluran['status'] == 'DIJADWALKAN'
? AppTheme.processedColor
: AppTheme.warningColor;
final IconData statusIcon = penyaluran['status'] == 'TERLAKSANA'
? Icons.check_circle
: penyaluran['status'] == 'DIJADWALKAN'
? Icons.event
: Icons.pending;
final Map<String, dynamic> stokBantuan =
penyaluran['stok_bantuan'] as Map<String, dynamic>;
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
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,
),
),
],
),
],
),
const Divider(height: 24),
// Informasi bantuan
Row(
crossAxisAlignment: CrossAxisAlignment.start,
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',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'${stokBantuan['jenis'] ?? 'Umum'}${stokBantuan['kuantitas'] ?? '1 Paket'}',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 8),
Text(
penyaluran['keterangan'] ?? '',
style: const TextStyle(
fontSize: 14,
),
),
],
),
),
],
),
// 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,
),
),
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'),
),
);
},
),
),
],
),
],
),
),
);
}
} }

View File

@ -93,6 +93,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
), ),
], ],
); );
// Tampilkan tombol riwayat hanya jika tab Penitipan aktif // Tampilkan tombol riwayat hanya jika tab Penitipan aktif
if (activeTab == 2) { if (activeTab == 2) {
return Row( return Row(
@ -167,146 +168,333 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Widget _buildDrawer(BuildContext context) { Widget _buildDrawer(BuildContext context) {
return Drawer( return Drawer(
child: ListView( child: Column(
padding: EdgeInsets.zero,
children: [ children: [
DrawerHeader( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: AppTheme.primaryGradient, gradient: AppTheme.primaryGradient,
), ),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 16,
bottom: 24,
left: 16,
right: 16),
width: double.infinity,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
CircleAvatar( Container(
radius: 30, decoration: BoxDecoration(
backgroundColor: Colors.white, shape: BoxShape.circle,
child: const Icon( border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Hero(
tag: 'profile-photo',
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person, Icons.person,
color: Colors.white,
size: 40, size: 40,
color: AppTheme.primaryColor, )
: null,
),
),
),
SizedBox(height: 16),
Text(
'Halo,',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
), ),
), ),
const SizedBox(height: 10),
Text( Text(
controller.nama, controller.nama,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
controller.user?.desa?.nama != null
? '${controller.formattedRole} - ${controller.user!.desa!.nama}'
: controller.formattedRole,
style: TextStyle( style: TextStyle(
color: Colors.white.withAlpha(200), color: Colors.white,
fontSize: 14, 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(
controller.formattedRole,
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,
style: TextStyle(
color: Colors.white,
fontSize: 12,
), ),
), ),
], ],
), ),
), ),
ListTile( ],
leading: const Icon(Icons.dashboard_outlined), ),
title: const Text('Dashboard'), ],
selected: controller.activeTabIndex.value == 0, ),
selectedColor: AppTheme.primaryColor, ),
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: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.changeTab(0); controller.changeTab(0);
}, },
), )),
ListTile( Obx(() => _buildMenuItem(
leading: const Icon(Icons.handshake_outlined), icon: Icons.handshake_outlined,
title: const Text('Penyaluran'), activeIcon: Icons.handshake,
selected: controller.activeTabIndex.value == 1, title: 'Penyaluran',
selectedColor: AppTheme.primaryColor, isSelected: controller.activeTabIndex.value == 1,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.changeTab(1); controller.changeTab(1);
}, },
), )),
ListTile( Obx(() => _buildMenuItem(
leading: const Icon(Icons.inventory_2_outlined), icon: Icons.inventory_2_outlined,
title: const Text('Penitipan'), activeIcon: Icons.inventory_2,
selected: controller.activeTabIndex.value == 2, title: 'Penitipan',
selectedColor: AppTheme.primaryColor, isSelected: controller.activeTabIndex.value == 2,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.changeTab(2); controller.changeTab(2);
}, },
), )),
Obx(() => ListTile( Obx(() => _buildMenuItem(
leading: controller.jumlahDiproses.value > 0 icon: Icons.warning_amber_outlined,
? Badge( activeIcon: Icons.warning_amber,
label: Text(controller.jumlahDiproses.value.toString()), title: 'Pengaduan',
backgroundColor: Colors.red, isSelected: controller.activeTabIndex.value == 3,
child: const Icon(Icons.warning_amber_outlined), badge: controller.jumlahDiproses.value > 0
) ? controller.jumlahDiproses.value.toString()
: const Icon(Icons.warning_amber_outlined), : null,
title: const Text('Pengaduan'),
selected: controller.activeTabIndex.value == 3,
selectedColor: AppTheme.primaryColor,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.changeTab(3); controller.changeTab(3);
}, },
)), )),
ListTile( Obx(() => _buildMenuItem(
leading: const Icon(Icons.inventory_outlined), icon: Icons.inventory_outlined,
title: const Text('Stok Bantuan'), activeIcon: Icons.inventory,
selected: controller.activeTabIndex.value == 4, title: 'Stok Bantuan',
selectedColor: AppTheme.primaryColor, isSelected: controller.activeTabIndex.value == 4,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.changeTab(4); controller.changeTab(4);
}, },
), )),
const Divider(), _buildMenuCategory('Kelola Data'),
ListTile( _buildMenuItem(
leading: const Icon(Icons.person_add_outlined), icon: Icons.person_add_outlined,
title: const Text('Kelola Penerima'), activeIcon: Icons.person_add,
title: 'Kelola Penerima',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/daftar-penerima'); Get.toNamed('/daftar-penerima');
}, },
), ),
ListTile( _buildMenuItem(
leading: const Icon(Icons.people_outlined), icon: Icons.people_outlined,
title: const Text('Kelola Donatur'), activeIcon: Icons.people,
title: 'Kelola Donatur',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/daftar-donatur'); Get.toNamed('/daftar-donatur');
}, },
), ),
ListTile( _buildMenuItem(
leading: const Icon(Icons.description_outlined), icon: Icons.description_outlined,
title: const Text('Laporan Penyaluran'), activeIcon: Icons.description,
title: 'Laporan Penyaluran',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/laporan-penyaluran'); Get.toNamed('/laporan-penyaluran');
}, },
), ),
ListTile( _buildMenuCategory('Pengaturan'),
leading: const Icon(Icons.person_outline), _buildMenuItem(
title: const Text('Profil'), icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/profile'); Get.toNamed('/profile');
}, },
), ),
ListTile( _buildMenuItem(
leading: const Icon(Icons.logout), icon: Icons.logout,
title: const Text('Keluar'), title: 'Keluar',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.logout(); 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: Stack(
alignment: Alignment.center,
children: [
Icon(
isSelected ? (activeIcon ?? icon) : icon,
color: isSelected
? AppTheme.primaryColor
: isLogout
? Colors.red
: Colors.grey[700],
size: 24,
),
if (badge != null)
Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
badge,
style: TextStyle(
fontSize: 10,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
),
],
),
title: Text(
title,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected
? AppTheme.primaryColor
: isLogout
? Colors.red
: Colors.grey[800],
fontSize: 14,
),
),
onTap: onTap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
visualDensity: VisualDensity.compact,
selectedTileColor: AppTheme.primaryColor.withOpacity(0.1),
selected: isSelected,
),
); );
} }
@ -456,6 +644,8 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
children: [ children: [
const Icon(Icons.report_problem_outlined), const Icon(Icons.report_problem_outlined),
// Selalu tampilkan badge untuk debugging // Selalu tampilkan badge untuk debugging
if (controller.jumlahDiproses.value > 0)
Positioned( Positioned(
top: 0, top: 0,
right: 0, right: 0,

View File

@ -3,16 +3,24 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
class ProfileController extends GetxController { class ProfileController extends GetxController {
final SupabaseService _supabaseService = Get.find<SupabaseService>(); final SupabaseService _supabaseService = Get.find<SupabaseService>();
final AuthController _authController = Get.find<AuthController>(); final AuthController _authController = Get.find<AuthController>();
final ImagePicker _imagePicker = ImagePicker();
final Rx<BaseUserModel?> user = Rx<BaseUserModel?>(null); final Rx<BaseUserModel?> user = Rx<BaseUserModel?>(null);
final RxBool isLoading = true.obs; final RxBool isLoading = true.obs;
final RxBool isEditing = false.obs; final RxBool isEditing = false.obs;
final Rx<Map<String, dynamic>?> roleData = Rx<Map<String, dynamic>?>(null); final Rx<Map<String, dynamic>?> roleData = Rx<Map<String, dynamic>?>(null);
// Untuk foto profil
final RxString fotoProfil = ''.obs;
final RxString fotoProfilPath = ''.obs;
final RxBool isUploadingFoto = false.obs;
// Form controllers // Form controllers
late TextEditingController nameController; late TextEditingController nameController;
late TextEditingController emailController; late TextEditingController emailController;
@ -51,10 +59,15 @@ class ProfileController extends GetxController {
if (userData['role_data'] != null) { if (userData['role_data'] != null) {
roleData.value = userData['role_data'] as Map<String, dynamic>?; roleData.value = userData['role_data'] as Map<String, dynamic>?;
// Jika role adalah warga, ambil no telepon dari role data // Jika role adalah warga, ambil no telepon dari role data
if (user.value?.role?.toLowerCase() == 'warga' && if (roleData.value?['no_hp'] != null) {
roleData.value?['no_hp'] != null) {
phoneController.text = roleData.value?['no_hp'] ?? ''; phoneController.text = roleData.value?['no_hp'] ?? '';
} }
// Ambil foto profil jika ada
if (roleData.value?['foto_profil'] != null) {
fotoProfil.value = roleData.value?['foto_profil'] ?? '';
print(fotoProfil.value);
}
} }
} }
} catch (e) { } catch (e) {
@ -74,6 +87,86 @@ class ProfileController extends GetxController {
isEditing.value = !isEditing.value; isEditing.value = !isEditing.value;
} }
// Metode untuk memilih foto profil dari kamera
Future<void> pickFotoProfilFromCamera() async {
try {
final pickedFile = await _imagePicker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
if (pickedFile != null) {
fotoProfilPath.value = pickedFile.path;
}
} catch (e) {
print('Error mengambil foto dari kamera: $e');
Get.snackbar(
'Error',
'Gagal mengambil foto: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Metode untuk memilih foto profil dari galeri
Future<void> pickFotoProfilFromGallery() async {
try {
final pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
if (pickedFile != null) {
fotoProfilPath.value = pickedFile.path;
}
} catch (e) {
print('Error mengambil foto dari galeri: $e');
Get.snackbar(
'Error',
'Gagal mengambil foto: ${e.toString()}',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Metode untuk menghapus foto profil
void clearFotoProfil() {
fotoProfilPath.value = '';
}
// Metode untuk mengupload foto profil
Future<String?> _uploadFotoProfil() async {
if (fotoProfilPath.isEmpty) return null;
try {
isUploadingFoto.value = true;
final userData = user.value;
if (userData == null) throw 'Data user tidak ditemukan';
// Upload foto ke Supabase storage
final fotoUrl = await _supabaseService.uploadFile(
fotoProfilPath.value,
'profiles', // bucket name
'profile_photos/${userData.id}', // folder path
);
return fotoUrl;
} catch (e) {
print('Error upload foto profil: $e');
throw e.toString();
} finally {
isUploadingFoto.value = false;
}
}
Future<void> updateProfile() async { Future<void> updateProfile() async {
if (nameController.text.isEmpty) { if (nameController.text.isEmpty) {
Get.snackbar( Get.snackbar(
@ -91,6 +184,15 @@ class ProfileController extends GetxController {
final userData = user.value; final userData = user.value;
if (userData == null) throw 'Data user tidak ditemukan'; if (userData == null) throw 'Data user tidak ditemukan';
// Upload foto profil jika ada
String? fotoProfilUrl;
if (fotoProfilPath.isNotEmpty) {
fotoProfilUrl = await _uploadFotoProfil();
if (fotoProfilUrl == null) {
throw 'Gagal mengupload foto profil';
}
}
// Update data sesuai role // Update data sesuai role
switch (userData.role?.toLowerCase() ?? 'unknown') { switch (userData.role?.toLowerCase() ?? 'unknown') {
case 'warga': case 'warga':
@ -99,6 +201,7 @@ class ProfileController extends GetxController {
namaLengkap: nameController.text, namaLengkap: nameController.text,
noHp: phoneController.text, noHp: phoneController.text,
email: emailController.text, email: emailController.text,
fotoProfil: fotoProfilUrl,
); );
break; break;
case 'donatur': case 'donatur':
@ -107,6 +210,7 @@ class ProfileController extends GetxController {
nama: nameController.text, nama: nameController.text,
noHp: phoneController.text, noHp: phoneController.text,
email: emailController.text, email: emailController.text,
fotoProfil: fotoProfilUrl,
); );
break; break;
case 'petugas_desa': case 'petugas_desa':
@ -115,6 +219,7 @@ class ProfileController extends GetxController {
nama: nameController.text, nama: nameController.text,
noHp: phoneController.text, noHp: phoneController.text,
email: emailController.text, email: emailController.text,
fotoProfil: fotoProfilUrl,
); );
break; break;
default: default:
@ -127,6 +232,9 @@ class ProfileController extends GetxController {
// Refresh data di AuthController untuk menyebarkan perubahan ke seluruh aplikasi // Refresh data di AuthController untuk menyebarkan perubahan ke seluruh aplikasi
await _authController.refreshUserData(); await _authController.refreshUserData();
// Reset path foto setelah update
fotoProfilPath.value = '';
// Keluar dari mode edit // Keluar dari mode edit
isEditing.value = false; isEditing.value = false;

View File

@ -2,10 +2,55 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/profile/controllers/profile_controller.dart'; import 'package:penyaluran_app/app/modules/profile/controllers/profile_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'dart:io';
class ProfileView extends GetView<ProfileController> { class ProfileView extends GetView<ProfileController> {
const ProfileView({super.key}); const ProfileView({super.key});
// Helper untuk mengkonversi nilai role ke tampilan yang lebih baik
String _formatRoleName(String? role) {
if (role == null) return 'Pengguna';
switch (role.toLowerCase()) {
case 'warga':
return 'Warga';
case 'petugas_desa':
return 'Petugas Desa';
case 'admin_desa':
return 'Admin Desa';
case 'donatur':
return 'Donatur';
case 'admin':
return 'Administrator';
default:
// Kapitalisasi setiap kata dan ganti underscore dengan spasi
return role
.split('_')
.map((word) => word.isEmpty
? ''
: '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}')
.join(' ');
}
}
// Helper untuk mendapatkan warna berdasarkan role
Color _getRoleColor(String? role) {
if (role == null) return AppTheme.primaryColor;
switch (role.toLowerCase()) {
case 'warga':
return AppTheme.infoColor;
case 'petugas_desa':
return AppTheme.primaryColor;
case 'donatur':
return AppTheme.successColor;
default:
return AppTheme.primaryColor;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -53,15 +98,71 @@ class ProfileView extends GetView<ProfileController> {
return Center( return Center(
child: Column( child: Column(
children: [ children: [
const CircleAvatar( Obx(() {
radius: 50, // Jika user sedang dalam mode edit dan sudah memilih foto baru
backgroundColor: AppTheme.primaryColor, if (controller.isEditing.value &&
child: Icon( controller.fotoProfilPath.isNotEmpty) {
Icons.person, return _buildProfileImage(
size: 60, isLocalFile: true,
color: Colors.white, imagePath: controller.fotoProfilPath.value,
);
}
// Jika user sudah memiliki foto profil
else if (controller.fotoProfil.isNotEmpty) {
return _buildProfileImage(
isLocalFile: false,
imagePath: controller.fotoProfil.value,
);
}
// Default jika tidak ada foto
else {
return _buildDefaultProfileImage();
}
}),
// Tombol edit foto (hanya muncul dalam mode edit)
Obx(() {
if (controller.isEditing.value) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Tombol ambil dari kamera
_buildPhotoActionButton(
icon: Icons.camera_alt,
label: 'Kamera',
onPressed: () => controller.pickFotoProfilFromCamera(),
),
const SizedBox(width: 8),
// Tombol ambil dari galeri
_buildPhotoActionButton(
icon: Icons.photo_library,
label: 'Galeri',
onPressed: () => controller.pickFotoProfilFromGallery(),
),
// Tombol hapus foto (hanya jika ada foto yang dipilih)
if (controller.fotoProfilPath.isNotEmpty ||
controller.fotoProfil.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: _buildPhotoActionButton(
icon: Icons.delete,
label: 'Hapus',
onPressed: () => controller.clearFotoProfil(),
color: Colors.red,
), ),
), ),
],
),
);
} else {
return const SizedBox.shrink();
}
}),
const SizedBox(height: 16), const SizedBox(height: 16),
Obx(() => Text( Obx(() => Text(
controller.user.value?.name ?? 'Pengguna', controller.user.value?.name ?? 'Pengguna',
@ -71,19 +172,151 @@ class ProfileView extends GetView<ProfileController> {
), ),
)), )),
const SizedBox(height: 4), const SizedBox(height: 4),
Obx(() => Text( Obx(() {
controller.user.value?.role?.toUpperCase() ?? 'PENGGUNA', final role = controller.user.value?.role;
final roleColor = _getRoleColor(role);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: roleColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: roleColor.withOpacity(0.3)),
),
child: Text(
_formatRoleName(role),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[600], color: roleColor,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
)), ),
);
}),
], ],
), ),
); );
} }
// Widget foto profil default
Widget _buildDefaultProfileImage() {
return CircleAvatar(
radius: 60,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: const Icon(
Icons.person,
size: 70,
color: AppTheme.primaryColor,
),
);
}
// Widget foto profil dengan gambar
Widget _buildProfileImage(
{required bool isLocalFile, required String imagePath}) {
return Stack(
children: [
// Widget untuk menampilkan foto profil
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.5),
width: 3,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(60),
child: isLocalFile
? Image.file(
File(imagePath),
fit: BoxFit.cover,
width: 120,
height: 120,
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
)
: Image.network(
imagePath,
fit: BoxFit.cover,
width: 120,
height: 120,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) =>
_buildDefaultProfileImage(),
),
),
),
// Indikator loading saat mengupload
if (controller.isUploadingFoto.value)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black38,
shape: BoxShape.circle,
),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
),
],
);
}
// Widget tombol aksi foto
Widget _buildPhotoActionButton({
required IconData icon,
required String label,
required VoidCallback onPressed,
Color? color,
}) {
final buttonColor = color ?? AppTheme.primaryColor;
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: buttonColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: buttonColor.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: buttonColor),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: buttonColor,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildProfileForm() { Widget _buildProfileForm() {
return Obx(() { return Obx(() {
final isEditing = controller.isEditing.value; final isEditing = controller.isEditing.value;
@ -222,9 +455,6 @@ class ProfileView extends GetView<ProfileController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildInfoRow(Icons.badge, 'NIP', roleData?['nip'] ?? '-'), _buildInfoRow(Icons.badge, 'NIP', roleData?['nip'] ?? '-'),
const SizedBox(height: 8),
_buildInfoRow(
Icons.work, 'Jabatan', roleData?['jabatan'] ?? '-'),
if (user.desa != null) ...[ if (user.desa != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
_buildInfoRow( _buildInfoRow(

View File

@ -3,6 +3,7 @@ import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/pengajuan_kelayakan_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/warga_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,6 +14,9 @@ class WargaDashboardController extends GetxController {
final Rx<BaseUserModel?> currentUser = Rx<BaseUserModel?>(null); final Rx<BaseUserModel?> currentUser = Rx<BaseUserModel?>(null);
// Variabel untuk foto profil
final RxString fotoProfil = ''.obs;
// Indeks tab yang aktif di bottom navigation bar // Indeks tab yang aktif di bottom navigation bar
final RxInt activeTabIndex = 0.obs; final RxInt activeTabIndex = 0.obs;
@ -55,6 +59,48 @@ class WargaDashboardController extends GetxController {
String? get desa => user?.desa?.nama; String? get desa => user?.desa?.nama;
// Getter untuk alamat dan noHp
String? get alamat {
if (_authController.isWarga && _authController.roleData != null) {
return (_authController.roleData as WargaModel).alamat;
}
return null;
}
String? get noHp {
if (_authController.isWarga && _authController.roleData != null) {
return (_authController.roleData as WargaModel).noHp;
}
return null;
}
// Getter untuk foto profil
String? get profilePhotoUrl {
// 1. Coba ambil dari fotoProfil yang sudah disimpan
if (fotoProfil.isNotEmpty) {
return fotoProfil.value;
}
// 2. Coba ambil dari roleData jika merupakan WargaModel
if (_authController.isWarga && _authController.roleData != null) {
final wargaData = _authController.roleData as WargaModel;
if (wargaData.fotoProfil != null && wargaData.fotoProfil!.isNotEmpty) {
return wargaData.fotoProfil;
}
}
// 3. Coba ambil dari userData.roleData.fotoProfil
final userData = _authController.userData;
if (userData != null && userData.roleData is WargaModel) {
final wargaData = userData.roleData as WargaModel;
if (wargaData.fotoProfil != null && wargaData.fotoProfil!.isNotEmpty) {
return wargaData.fotoProfil;
}
}
return null;
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -81,12 +127,45 @@ class WargaDashboardController extends GetxController {
print('DEBUG WARGA: User adalah warga'); print('DEBUG WARGA: User adalah warga');
var wargaData = _authController.roleData; var wargaData = _authController.roleData;
print('DEBUG WARGA: Data warga: ${wargaData?.namaLengkap}'); print('DEBUG WARGA: Data warga: ${wargaData?.namaLengkap}');
// Ambil foto profil dari wargaData jika ada
if (wargaData != null &&
wargaData.fotoProfil != null &&
wargaData.fotoProfil!.isNotEmpty) {
fotoProfil.value = wargaData.fotoProfil!;
print('DEBUG WARGA: Foto profil: ${fotoProfil.value}');
}
} else { } else {
print('DEBUG WARGA: User bukan warga'); print('DEBUG WARGA: User bukan warga');
} }
} else { } else {
print('DEBUG WARGA: userData null'); print('DEBUG WARGA: userData null');
} }
// Cek dan ambil foto profil jika belum ada
if (fotoProfil.isEmpty) {
_fetchProfilePhoto();
}
}
// Metode untuk mengambil foto profil
Future<void> _fetchProfilePhoto() async {
try {
if (user?.id == null) return;
final wargaData = await _supabaseService.client
.from('warga')
.select('foto_profil')
.eq('user_id', user!.id)
.single();
if (wargaData != null && wargaData['foto_profil'] != null) {
fotoProfil.value = wargaData['foto_profil'];
print('DEBUG WARGA: Foto profil dari API: ${fotoProfil.value}');
}
} catch (e) {
print('Error fetching profile photo: $e');
}
} }
void fetchData() async { void fetchData() async {

View File

@ -41,25 +41,66 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
} }
Widget _buildWelcomeSection() { Widget _buildWelcomeSection() {
return Card( return Container(
elevation: 2, margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [Colors.white, Colors.blue.shade50],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
), ),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
CircleAvatar( Container(
radius: 24, decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.blue.shade200, width: 2),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Hero(
tag: 'warga-profile',
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.blue.shade100, backgroundColor: Colors.blue.shade100,
child: Icon( backgroundImage: controller.profilePhotoUrl != null
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person, Icons.person,
color: Colors.blue.shade700, color: Colors.blue.shade700,
size: 28, size: 30,
)
: null,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -71,14 +112,16 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
'Selamat Datang,', 'Selamat Datang,',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey.shade600, color: Colors.blue.shade700,
fontWeight: FontWeight.w500,
), ),
), ),
Text( Text(
controller.nama, controller.nama,
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade900,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -88,64 +131,166 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 20),
const Divider(), Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
_buildInfoRow(
icon: Icons.home_rounded,
iconColor: Colors.blue.shade300,
label: 'Alamat',
value: controller.alamat ?? 'Alamat tidak tersedia',
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(height: 1),
),
_buildInfoRow(
icon: Icons.phone_rounded,
iconColor: Colors.green.shade300,
label: 'No. HP',
value: controller.noHp ?? 'No. HP tidak tersedia',
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(height: 1),
),
_buildInfoRow(
icon: Icons.location_city_rounded,
iconColor: Colors.amber.shade300,
label: 'Desa',
value: controller.desa ?? 'Desa tidak tersedia',
),
],
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Icon(
Icons.home,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: _buildActionButton(
'Alamat tidak tersedia', icon: Icons.edit_rounded,
label: 'Edit Profil',
color: Colors.blue.shade700,
onTap: () => Get.toNamed(Routes.PROFILE),
),
),
const SizedBox(width: 10),
Expanded(
child: _buildActionButton(
icon: Icons.notifications_rounded,
label: 'Notifikasi',
color: Colors.amber.shade700,
onTap: () => Get.toNamed(Routes.NOTIFIKASI),
),
),
],
),
],
),
),
),
);
}
Widget _buildInfoRow({
required IconData icon,
required Color iconColor,
required String label,
required String value,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 16,
color: iconColor,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle( style: TextStyle(
color: Colors.grey.shade700, fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
], ],
), ),
const SizedBox(height: 8), ),
Row( ],
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Ink(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.phone, icon,
size: 16, size: 18,
color: Colors.grey.shade600, color: color,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'No. HP tidak tersedia', label,
style: TextStyle( style: TextStyle(
color: Colors.grey.shade700, color: color,
fontWeight: FontWeight.w600,
), ),
), ),
], ],
), ),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.location_city,
size: 16,
color: Colors.grey.shade600,
),
const SizedBox(width: 8),
Text(
controller.desa ?? 'Desa tidak tersedia',
style: TextStyle(
color: Colors.grey.shade700,
),
),
],
),
],
), ),
), ),
); );

View File

@ -33,6 +33,11 @@ abstract class Routes {
static const riwayatPengaduan = _Paths.riwayatPengaduan; static const riwayatPengaduan = _Paths.riwayatPengaduan;
static const qrScanner = _Paths.qrScanner; static const qrScanner = _Paths.qrScanner;
static const konfirmasiPenerimaQr = _Paths.konfirmasiPenerimaQr; static const konfirmasiPenerimaQr = _Paths.konfirmasiPenerimaQr;
static const PENGADUAN = '/pengaduan';
static const TAMBAH_PENGADUAN = '/tambah-pengaduan';
static const PENGADUAN_DETAIL = '/pengaduan-detail';
static const PROFILE = '/profile';
static const NOTIFIKASI = '/notifikasi';
} }
abstract class _Paths { abstract class _Paths {
@ -68,4 +73,9 @@ abstract class _Paths {
static const riwayatPengaduan = '/petugas-desa/riwayat-pengaduan'; static const riwayatPengaduan = '/petugas-desa/riwayat-pengaduan';
static const qrScanner = '/petugas-desa/qr-scanner'; static const qrScanner = '/petugas-desa/qr-scanner';
static const konfirmasiPenerimaQr = '/petugas-desa/konfirmasi-penerima/:id'; static const konfirmasiPenerimaQr = '/petugas-desa/konfirmasi-penerima/:id';
static const PENGADUAN = '/pengaduan';
static const TAMBAH_PENGADUAN = '/tambah-pengaduan';
static const PENGADUAN_DETAIL = '/pengaduan-detail';
static const PROFILE = '/profile';
static const NOTIFIKASI = '/notifikasi';
} }

View File

@ -911,7 +911,7 @@ class SupabaseService extends GetxService {
final response = await client final response = await client
.from('donatur') .from('donatur')
.select('*') .select('*')
.order('nama', ascending: true); .order('nama_lengkap', ascending: true);
return response; return response;
} catch (e) { } catch (e) {
@ -1105,6 +1105,23 @@ class SupabaseService extends GetxService {
} }
} }
// Metode untuk mendapatkan data penyaluran bantuan berdasarkan ID warga
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);
return response;
} catch (e) {
print('Error getting penyaluran bantuan by warga id: $e');
return null;
}
}
Future<void> tambahPenerima(Map<String, dynamic> penerima) async { Future<void> tambahPenerima(Map<String, dynamic> penerima) async {
try { try {
await client.from('warga').insert(penerima); await client.from('warga').insert(penerima);
@ -1616,103 +1633,108 @@ class SupabaseService extends GetxService {
Future<void> updateWargaProfile({ Future<void> updateWargaProfile({
required String userId, required String userId,
required String namaLengkap, required String namaLengkap,
required String email,
String? noHp, String? noHp,
String? email, String? fotoProfil,
String? alamat,
String? nik,
String? tempatLahir,
DateTime? tanggalLahir,
String? jenisKelamin,
String? agama,
String? kategoriEkonomi,
}) async { }) async {
try { try {
final data = { // Buat map untuk update data
final Map<String, dynamic> updateData = {
'nama_lengkap': namaLengkap, 'nama_lengkap': namaLengkap,
'no_hp': noHp,
'updated_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(),
}; };
if (noHp != null) data['no_hp'] = noHp; // Tambahkan foto profil jika ada
if (email != null) data['email'] = email; if (fotoProfil != null) {
if (alamat != null) data['alamat'] = alamat; updateData['foto_profil'] = fotoProfil;
if (nik != null) data['nik'] = nik; }
if (tempatLahir != null) data['tempat_lahir'] = tempatLahir;
if (tanggalLahir != null)
data['tanggal_lahir'] = tanggalLahir.toIso8601String();
if (jenisKelamin != null) data['jenis_kelamin'] = jenisKelamin;
if (agama != null) data['agama'] = agama;
if (kategoriEkonomi != null) data['kategori_ekonomi'] = kategoriEkonomi;
await client.from('warga').update(data).eq('id', userId); // Update data warga
await client.from('warga').update(updateData).eq('id', userId);
// Hapus cache setelah update // Update email di auth.users jika berubah
clearUserProfileCache(); if (email != client.auth.currentUser?.email) {
// Gunakan metode updateUserEmail
await client.auth.updateUser(UserAttributes(
email: email,
));
}
} catch (e) { } catch (e) {
print('Error updating warga profile: $e'); print('Error updating warga profile: $e');
throw e.toString(); throw e.toString();
} }
} }
// Metode untuk update profil donatur // Metode untuk memperbarui profil donatur
Future<void> updateDonaturProfile({ Future<void> updateDonaturProfile({
required String userId, required String userId,
required String nama, required String nama,
String? alamat, required String email,
String? noHp, String? noHp,
String? email, String? fotoProfil,
String? jenis,
String? instansi,
String? jabatan,
}) async { }) async {
try { try {
final data = { // Buat map untuk update data
final Map<String, dynamic> updateData = {
'nama': nama, 'nama': nama,
'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap
'no_hp': noHp,
'updated_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(),
}; };
if (alamat != null) data['alamat'] = alamat; // Tambahkan foto profil jika ada
if (noHp != null) data['no_hp'] = noHp; if (fotoProfil != null) {
if (email != null) data['email'] = email; updateData['foto_profil'] = fotoProfil;
if (jenis != null) data['jenis'] = jenis; }
if (instansi != null) data['instansi'] = instansi;
if (jabatan != null) data['jabatan'] = jabatan;
await client.from('donatur').update(data).eq('id', userId); // Update data donatur
await client.from('donatur').update(updateData).eq('id', userId);
// Hapus cache setelah update // Update email di auth.users jika berubah
clearUserProfileCache(); if (email != client.auth.currentUser?.email) {
// Gunakan metode updateUserEmail
await client.auth.updateUser(UserAttributes(
email: email,
));
}
} catch (e) { } catch (e) {
print('Error updating donatur profile: $e'); print('Error updating donatur profile: $e');
throw e.toString(); throw e.toString();
} }
} }
// Metode untuk update profil petugas desa // Metode untuk memperbarui profil petugas desa
Future<void> updatePetugasDesaProfile({ Future<void> updatePetugasDesaProfile({
required String userId, required String userId,
required String nama, required String nama,
String? alamat, required String email,
String? noHp, String? noHp,
String? email, String? fotoProfil,
String? nip,
String? jabatan,
}) async { }) async {
try { try {
final data = { // Buat map untuk update data
'nama': nama, final Map<String, dynamic> updateData = {
'nama_lengkap': nama, // Untuk konsistensi dengan field nama_lengkap
'no_hp': noHp,
'updated_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(),
}; };
if (alamat != null) data['alamat'] = alamat; // Tambahkan foto profil jika ada
if (noHp != null) data['no_hp'] = noHp; if (fotoProfil != null) {
if (email != null) data['email'] = email; updateData['foto_profil'] = fotoProfil;
if (nip != null) data['nip'] = nip; }
if (jabatan != null) data['jabatan'] = jabatan;
await client.from('petugas_desa').update(data).eq('id', userId); // Update data petugas desa
await client.from('petugas_desa').update(updateData).eq('id', userId);
// Hapus cache setelah update // Update email di auth.users jika berubah
clearUserProfileCache(); if (email != client.auth.currentUser?.email) {
// Gunakan metode updateUserEmail
await client.auth.updateUser(UserAttributes(
email: email,
));
}
} catch (e) { } catch (e) {
print('Error updating petugas desa profile: $e'); print('Error updating petugas desa profile: $e');
throw e.toString(); throw e.toString();