Perbarui model PenitipanBantuan untuk menggunakan stok_bantuan sebagai kategori bantuan. Tambahkan rute baru untuk Donatur dengan beberapa halaman terkait, termasuk donaturDashboard, donaturSkema, donaturJadwal, donaturPenitipan, dan donaturLaporan. Modifikasi rute dan tampilan untuk meningkatkan navigasi dan pengalaman pengguna.
This commit is contained in:
0
DonaturDashboardController())
Normal file
0
DonaturDashboardController())
Normal file
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ɚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt <08>ɚ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08>ɚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08>ɚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
n
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08>ɚ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08>ɚ<EFBFBD><EFBFBD>2y
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
w
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08>ɚ<EFBFBD><EFBFBD>2
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
|
||||
x
|
||||
x
|
||||
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 ~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||
u
|
||||
u
|
||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>Ԛ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <08>Ԛ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json <08>Ԛ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <08>Ԛ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
p
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja <08>Ԛ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt <08>Ԛ<EFBFBD><EFBFBD>2{
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt <08>Ԛ<EFBFBD><EFBFBD>2
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
||||
z
|
||||
z
|
||||
xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 <09>
|
||||
~
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
w
|
||||
w
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ښ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt <08>ښ<EFBFBD><EFBFBD>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><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2x
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json <08>ښ<EFBFBD><EFBFBD>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><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>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><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
|
||||
h
|
||||
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja <08>ښ<EFBFBD><EFBFBD>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><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
|
||||
l
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08>ښ<EFBFBD><EFBFBD>2s
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2s
|
||||
q
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08>ښ<EFBFBD><EFBFBD>2
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 x
|
||||
v
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
o
|
||||
o
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ߚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint ِ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt <08>ߚ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt ِ<EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json <08>ߚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json ِ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json <08>ߚ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json ِ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
k
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja <08>ߚ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja ِ<EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
o
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt <08>ߚ<EFBFBD><EFBFBD>2v
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt ِ<EFBFBD><EFBFBD><EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt <08>ߚ<EFBFBD><EFBFBD>2
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt ِ<EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||
u
|
||||
u
|
||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json ِ<><D990><EFBFBD>2 {
|
||||
y
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin ِ<><D990><EFBFBD>2
|
||||
<EFBFBD>
|
||||
|
||||
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt ِ<><D990><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 ِ<><D990><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
@ -78,8 +78,8 @@ class PenitipanBantuanModel {
|
||||
donatur: json["donatur"] != null
|
||||
? DonaturModel.fromJson(json["donatur"])
|
||||
: null,
|
||||
kategoriBantuan: json["kategori_bantuan"] != null
|
||||
? KategoriBantuanModel.fromJson(json["kategori_bantuan"])
|
||||
kategoriBantuan: json["stok_bantuan"] != null
|
||||
? KategoriBantuanModel.fromJson(json["stok_bantuan"])
|
||||
: null,
|
||||
isUang: json["is_uang"] ?? false,
|
||||
);
|
||||
|
20
lib/app/modules/donatur/bindings/donatur_binding.dart
Normal file
20
lib/app/modules/donatur/bindings/donatur_binding.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
|
||||
class DonaturBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Hapus controller lama jika sudah ada
|
||||
if (Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
Get.delete<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
// Pasang controller baru
|
||||
Get.put<DonaturDashboardController>(
|
||||
DonaturDashboardController(),
|
||||
permanent: true,
|
||||
tag: 'donatur_dashboard',
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,457 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.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/routes/app_pages.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class DonaturDashboardController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
final SupabaseService _supabaseService = SupabaseService.to;
|
||||
|
||||
final Rx<BaseUserModel?> currentUser = Rx<BaseUserModel?>(null);
|
||||
|
||||
// Variabel untuk foto profil
|
||||
final RxString fotoProfil = ''.obs;
|
||||
|
||||
// Indeks tab yang aktif di bottom navigation bar
|
||||
final RxInt activeTabIndex = 0.obs;
|
||||
|
||||
// Data untuk skema bantuan tersedia
|
||||
final RxList<SkemaBantuanModel> skemaBantuan = <SkemaBantuanModel>[].obs;
|
||||
|
||||
// Data untuk jadwal penyaluran
|
||||
final RxList<PenyaluranBantuanModel> jadwalPenyaluran =
|
||||
<PenyaluranBantuanModel>[].obs;
|
||||
|
||||
// Data untuk riwayat penitipan bantuan
|
||||
final RxList<PenitipanBantuanModel> penitipanBantuan =
|
||||
<PenitipanBantuanModel>[].obs;
|
||||
|
||||
// Data untuk laporan penyaluran
|
||||
final RxList<LaporanPenyaluranModel> laporanPenyaluran =
|
||||
<LaporanPenyaluranModel>[].obs;
|
||||
|
||||
// Data untuk stok bantuan yang tersedia
|
||||
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
|
||||
|
||||
// Indikator loading
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Jumlah notifikasi belum dibaca
|
||||
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
|
||||
|
||||
// Data untuk foto bantuan pada form penitipan
|
||||
final RxList<String> fotoBantuanPaths = <String>[].obs;
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
// Getter untuk data user
|
||||
BaseUserModel? get user => _authController.baseUser;
|
||||
String get role => user?.role ?? 'DONATUR';
|
||||
String get nama {
|
||||
// Gunakan namaLengkap dari roleData jika tersedia
|
||||
if (_authController.isDonatur && _authController.roleData != null) {
|
||||
return _authController.roleData.namaLengkap ??
|
||||
_authController.displayName;
|
||||
}
|
||||
// Gunakan displayName dari AuthController
|
||||
return _authController.displayName;
|
||||
}
|
||||
|
||||
String? get desa => user?.desa?.nama;
|
||||
|
||||
// Getter untuk alamat dan noHp
|
||||
String? get alamat {
|
||||
if (_authController.isDonatur && _authController.roleData != null) {
|
||||
return (_authController.roleData as DonaturModel).alamat;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get noHp {
|
||||
if (_authController.isDonatur && _authController.roleData != null) {
|
||||
return (_authController.roleData as DonaturModel).noHp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Getter untuk jenis donatur
|
||||
String? get jenis {
|
||||
if (_authController.isDonatur && _authController.roleData != null) {
|
||||
return (_authController.roleData as DonaturModel).jenis;
|
||||
}
|
||||
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 DonaturModel
|
||||
if (_authController.isDonatur && _authController.roleData != null) {
|
||||
final donaturData = _authController.roleData as DonaturModel;
|
||||
if (donaturData.fotoProfil != null &&
|
||||
donaturData.fotoProfil!.isNotEmpty) {
|
||||
return donaturData.fotoProfil;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Coba ambil dari userData.roleData.fotoProfil
|
||||
final userData = _authController.userData;
|
||||
if (userData != null && userData.roleData is DonaturModel) {
|
||||
final donaturData = userData.roleData as DonaturModel;
|
||||
if (donaturData.fotoProfil != null &&
|
||||
donaturData.fotoProfil!.isNotEmpty) {
|
||||
return donaturData.fotoProfil;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchData();
|
||||
loadUserData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// Perbarui data user dan foto profil saat halaman siap
|
||||
loadUserData();
|
||||
}
|
||||
|
||||
void loadUserData() {
|
||||
currentUser.value = _authController.baseUser;
|
||||
|
||||
if (_authController.userData != null) {
|
||||
if (_authController.isDonatur) {
|
||||
var donaturData = _authController.roleData;
|
||||
|
||||
// Ambil foto profil dari donaturData jika ada
|
||||
if (donaturData != null &&
|
||||
donaturData.fotoProfil != null &&
|
||||
donaturData.fotoProfil!.isNotEmpty) {
|
||||
fotoProfil.value = donaturData.fotoProfil!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil foto profil dari database
|
||||
_fetchProfilePhoto();
|
||||
}
|
||||
|
||||
// Metode untuk mengambil foto profil
|
||||
Future<void> _fetchProfilePhoto() async {
|
||||
try {
|
||||
if (user?.id == null) return;
|
||||
|
||||
final donaturData = await _supabaseService.client
|
||||
.from('donatur')
|
||||
.select('foto_profil')
|
||||
.eq('id', user!.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (donaturData != null && donaturData['foto_profil'] != null) {
|
||||
fotoProfil.value = donaturData['foto_profil'];
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching profile photo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void fetchData() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Pastikan user sudah login dan memiliki ID
|
||||
if (user?.id == null) {
|
||||
throw Exception('User tidak terautentikasi');
|
||||
}
|
||||
|
||||
// Ambil data skema bantuan
|
||||
await fetchSkemaBantuan();
|
||||
|
||||
// Ambil data jadwal penyaluran
|
||||
await fetchJadwalPenyaluran();
|
||||
|
||||
// Ambil data penitipan bantuan
|
||||
await fetchPenitipanBantuan();
|
||||
|
||||
// Ambil data laporan penyaluran
|
||||
await fetchLaporanPenyaluran();
|
||||
|
||||
// Ambil data stok bantuan
|
||||
await fetchStokBantuan();
|
||||
|
||||
// Ambil data notifikasi
|
||||
await fetchNotifikasi();
|
||||
} catch (e) {
|
||||
print('Error fetching data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data skema bantuan
|
||||
Future<void> fetchSkemaBantuan() async {
|
||||
try {
|
||||
final response = await _supabaseService.client
|
||||
.from('xx02_skema_bantuan')
|
||||
.select()
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
skemaBantuan.value = response
|
||||
.map((data) => SkemaBantuanModel.fromJson(data))
|
||||
.toList()
|
||||
.cast<SkemaBantuanModel>();
|
||||
} catch (e) {
|
||||
print('Error fetching skema bantuan: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data jadwal penyaluran
|
||||
Future<void> fetchJadwalPenyaluran() async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final response = await _supabaseService.client
|
||||
.from('penyaluran_bantuan')
|
||||
.select()
|
||||
.order('tanggal_penyaluran', ascending: true);
|
||||
|
||||
// Konversi ke model lalu filter di sisi client
|
||||
final allJadwal = response
|
||||
.map((data) => PenyaluranBantuanModel.fromJson(data))
|
||||
.toList()
|
||||
.cast<PenyaluranBantuanModel>();
|
||||
|
||||
// Filter jadwal yang tanggalnya lebih besar dari hari ini
|
||||
jadwalPenyaluran.value = allJadwal
|
||||
.where((jadwal) =>
|
||||
jadwal.tanggalPenyaluran != null &&
|
||||
jadwal.tanggalPenyaluran!.isAfter(now))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error fetching jadwal penyaluran: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data penitipan bantuan
|
||||
Future<void> fetchPenitipanBantuan() async {
|
||||
try {
|
||||
if (user?.id == null) return;
|
||||
|
||||
final response = await _supabaseService.client
|
||||
.from('penitipan_bantuan')
|
||||
.select('*, donatur(*), stok_bantuan:stok_bantuan_id(*)')
|
||||
.eq('donatur_id', user!.id)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
penitipanBantuan.value = response
|
||||
.map((data) => PenitipanBantuanModel.fromJson(data))
|
||||
.toList()
|
||||
.cast<PenitipanBantuanModel>();
|
||||
} catch (e) {
|
||||
print('Error fetching penitipan bantuan: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data laporan penyaluran
|
||||
Future<void> fetchLaporanPenyaluran() async {
|
||||
try {
|
||||
final response = await _supabaseService.client
|
||||
.from('laporan_penyaluran')
|
||||
.select()
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
laporanPenyaluran.value = response
|
||||
.map((data) => LaporanPenyaluranModel.fromJson(data))
|
||||
.toList()
|
||||
.cast<LaporanPenyaluranModel>();
|
||||
} catch (e) {
|
||||
print('Error fetching laporan penyaluran: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data stok bantuan
|
||||
Future<void> fetchStokBantuan() async {
|
||||
try {
|
||||
final response = await _supabaseService.client
|
||||
.from('stok_bantuan')
|
||||
.select('*, kategori_bantuan:kategori_bantuan_id(*)')
|
||||
.order('nama', ascending: true);
|
||||
|
||||
stokBantuan.value = response
|
||||
.map((data) => StokBantuanModel.fromJson(data))
|
||||
.toList()
|
||||
.cast<StokBantuanModel>();
|
||||
} catch (e) {
|
||||
print('Error fetching stok bantuan: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil data notifikasi
|
||||
Future<void> fetchNotifikasi() async {
|
||||
try {
|
||||
if (user?.id == null) return;
|
||||
|
||||
final response = await _supabaseService.client
|
||||
.from('notifikasi')
|
||||
.select('*')
|
||||
.eq('user_id', user!.id)
|
||||
.eq('is_read', false)
|
||||
.count();
|
||||
|
||||
jumlahNotifikasiBelumDibaca.value = response.count ?? 0;
|
||||
} catch (e) {
|
||||
print('Error fetching notifikasi: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi untuk logout
|
||||
void logout() async {
|
||||
try {
|
||||
await _authController.logout();
|
||||
Get.offAllNamed(Routes.login);
|
||||
} catch (e) {
|
||||
print('Error during logout: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Terjadi kesalahan saat logout: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mendapatkan daftar stok bantuan yang tersedia
|
||||
List<StokBantuanModel> getAvailableStokBantuan() {
|
||||
// Filter stok bantuan yang jumlahnya lebih dari 0
|
||||
return stokBantuan.where((stok) => (stok.totalStok ?? 0) > 0).toList();
|
||||
}
|
||||
|
||||
// Ambil gambar dari kamera atau galeri
|
||||
Future<void> pickImage({required bool isCamera}) async {
|
||||
try {
|
||||
final source = isCamera ? ImageSource.camera : ImageSource.gallery;
|
||||
final pickedFile = await _imagePicker.pickImage(
|
||||
source: source,
|
||||
imageQuality: 70, // Kurangi kualitas untuk menghemat ukuran
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
fotoBantuanPaths.add(pickedFile.path);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error picking image: $e');
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat mengambil gambar',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus foto bantuan dari daftar
|
||||
void removeFotoBantuan(int index) {
|
||||
if (index >= 0 && index < fotoBantuanPaths.length) {
|
||||
fotoBantuanPaths.removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset foto bantuan paths
|
||||
void resetFotoBantuan() {
|
||||
fotoBantuanPaths.clear();
|
||||
}
|
||||
|
||||
// Membuat penitipan bantuan baru
|
||||
Future<void> createPenitipanBantuan(
|
||||
String? stokBantuanId,
|
||||
double jumlah,
|
||||
String deskripsi,
|
||||
String? skemaBantuanId,
|
||||
) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
if (user?.id == null) {
|
||||
throw Exception('User tidak terautentikasi');
|
||||
}
|
||||
|
||||
if (stokBantuanId == null) {
|
||||
throw Exception('Stok bantuan harus dipilih');
|
||||
}
|
||||
|
||||
if (fotoBantuanPaths.isEmpty) {
|
||||
throw Exception('Foto bantuan harus diunggah');
|
||||
}
|
||||
|
||||
// Unggah foto bantuan ke storage menggunakan metode dari SupabaseService
|
||||
final fotoBantuanUrls = await _supabaseService.uploadMultipleFiles(
|
||||
fotoBantuanPaths, 'penitipan', 'foto_bantuan');
|
||||
|
||||
// Data yang akan disimpan
|
||||
final Map<String, dynamic> data = {
|
||||
'donatur_id': user!.id,
|
||||
'stok_bantuan_id': stokBantuanId,
|
||||
'jumlah': jumlah,
|
||||
'deskripsi': deskripsi,
|
||||
'status': 'MENUNGGU',
|
||||
'tanggal_penitipan': DateTime.now().toIso8601String(),
|
||||
'foto_bantuan': fotoBantuanUrls,
|
||||
};
|
||||
|
||||
// Tambahkan skema bantuan jika ada
|
||||
if (skemaBantuanId != null && skemaBantuanId.isNotEmpty) {
|
||||
data['skema_bantuan_id'] = skemaBantuanId;
|
||||
}
|
||||
|
||||
// Simpan ke database
|
||||
await _supabaseService.client.from('penitipan_bantuan').insert(data);
|
||||
|
||||
// Reset foto bantuan setelah berhasil disimpan
|
||||
resetFotoBantuan();
|
||||
|
||||
// Ambil data penitipan bantuan yang baru
|
||||
await fetchPenitipanBantuan();
|
||||
|
||||
// Tampilkan pesan sukses
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
// Pindah ke tab riwayat penitipan
|
||||
DefaultTabController.of(Get.context!)?.animateTo(0);
|
||||
} catch (e) {
|
||||
print('Error creating penitipan bantuan: $e');
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat mengirim penitipan bantuan: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
774
lib/app/modules/donatur/views/donatur_dashboard_view.dart
Normal file
774
lib/app/modules/donatur/views/donatur_dashboard_view.dart
Normal file
@ -0,0 +1,774 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
const DonaturDashboardView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
controller.fetchData();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatisticSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildUpcomingEvents(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRecentPenitipan(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeSection() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
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: 'donatur-profile',
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
backgroundImage: controller.profilePhotoUrl != null
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Selamat Datang,',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.nama,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
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.category_rounded,
|
||||
iconColor: Colors.amber.shade300,
|
||||
label: 'Jenis Donatur',
|
||||
value: controller.jenis ?? 'Jenis tidak tersedia',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
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.add_box_rounded,
|
||||
label: 'Titip Bantuan',
|
||||
color: Colors.green.shade700,
|
||||
onTap: () {
|
||||
// Navigasi ke form penitipan bantuan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatisticSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Statistik Kontribusi',
|
||||
),
|
||||
Text(
|
||||
'Ringkasan aktivitas anda sebagai donatur',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Total Bantuan',
|
||||
value: '${controller.penitipanBantuan.length}',
|
||||
icon: Icons.card_giftcard,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Menunggu Verifikasi',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'MENUNGGU').length}',
|
||||
icon: Icons.hourglass_empty,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Diterima',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Ditolak',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITOLAK').length}',
|
||||
icon: Icons.cancel_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpcomingEvents() {
|
||||
final upcomingEvents = controller.jadwalPenyaluran
|
||||
.where((event) =>
|
||||
event.tanggalPenyaluran != null &&
|
||||
event.tanggalPenyaluran!.isAfter(DateTime.now()))
|
||||
.take(3)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Jadwal Penyaluran',
|
||||
),
|
||||
Text(
|
||||
'Jadwal penyaluran bantuan terdekat',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke tab jadwal penyaluran
|
||||
controller.activeTabIndex.value = 2;
|
||||
},
|
||||
child: Text(
|
||||
'Lihat Semua',
|
||||
style: TextStyle(color: Colors.blue.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (upcomingEvents.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Tidak ada jadwal penyaluran dalam waktu dekat',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...upcomingEvents.map((event) => _buildEventCard(event)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentPenitipan() {
|
||||
final recentPenitipan = controller.penitipanBantuan.take(3).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Bantuan Terakhir',
|
||||
),
|
||||
Text(
|
||||
'Riwayat penitipan bantuan terakhir',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigasi ke tab riwayat penitipan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
child: Text(
|
||||
'Lihat Semua',
|
||||
style: TextStyle(color: Colors.blue.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (recentPenitipan.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Belum ada riwayat penitipan bantuan',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...recentPenitipan.map((penitipan) => _buildPenitipanCard(penitipan)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: color,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard(dynamic event) {
|
||||
final formattedDate = event.tanggalPenyaluran != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
Text(
|
||||
event.tanggalPenyaluran != null
|
||||
? DateFormat('dd').format(event.tanggalPenyaluran!)
|
||||
: '--',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.nama ?? 'Penyaluran Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
event.deskripsi ?? 'Tidak ada deskripsi',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanCard(dynamic penitipan) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
Color statusColor;
|
||||
IconData statusIcon;
|
||||
|
||||
switch (penitipan.status) {
|
||||
case 'DITERIMA':
|
||||
statusColor = Colors.green;
|
||||
statusIcon = Icons.check_circle;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusColor = Colors.red;
|
||||
statusIcon = Icons.cancel;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusColor = Colors.orange;
|
||||
statusIcon = Icons.hourglass_empty;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
statusIcon,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penitipan.kategoriBantuan?.nama ?? 'Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
penitipan.status ?? 'MENUNGGU',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jumlah: ${penitipan.jumlah ?? 0}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
322
lib/app/modules/donatur/views/donatur_jadwal_view.dart
Normal file
322
lib/app/modules/donatur/views/donatur_jadwal_view.dart
Normal file
@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
const DonaturJadwalView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchJadwalPenyaluran();
|
||||
},
|
||||
child: controller.jadwalPenyaluran.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildJadwalList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event_busy,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Jadwal Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Jadwal penyaluran bantuan belum tersedia saat ini',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchJadwalPenyaluran(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalList() {
|
||||
// Kelompokkan jadwal berdasarkan bulan
|
||||
Map<String, List<dynamic>> groupedJadwal = {};
|
||||
|
||||
for (var jadwal in controller.jadwalPenyaluran) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
String monthYear =
|
||||
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!);
|
||||
|
||||
if (!groupedJadwal.containsKey(monthYear)) {
|
||||
groupedJadwal[monthYear] = [];
|
||||
}
|
||||
|
||||
groupedJadwal[monthYear]!.add(jadwal);
|
||||
}
|
||||
}
|
||||
|
||||
// Urutkan kunci (bulan) secara kronologis
|
||||
List<String> sortedMonths = groupedJadwal.keys.toList()
|
||||
..sort((a, b) {
|
||||
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
||||
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Jadwal Penyaluran Bantuan'),
|
||||
Text(
|
||||
'Daftar jadwal penyaluran bantuan yang akan datang',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tampilkan jadwal berdasarkan bulan
|
||||
...sortedMonths
|
||||
.map((month) => _buildMonthSection(month, groupedJadwal[month]!)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthSection(String month, List<dynamic> jadwalList) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
month,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
...jadwalList.map((jadwal) => _buildJadwalCard(jadwal)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalCard(dynamic jadwal) {
|
||||
final formattedDate = jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
String statusText = 'Akan Datang';
|
||||
Color statusColor = Colors.blue;
|
||||
|
||||
switch (jadwal.status) {
|
||||
case 'SELESAI':
|
||||
statusText = 'Selesai';
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'DIBATALKAN':
|
||||
statusText = 'Dibatalkan';
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
case 'DALAM_PROSES':
|
||||
statusText = 'Dalam Proses';
|
||||
statusColor = Colors.orange;
|
||||
break;
|
||||
default:
|
||||
statusText = 'Akan Datang';
|
||||
statusColor = Colors.blue;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (jadwal.tanggalPenyaluran != null) ...[
|
||||
Text(
|
||||
DateFormat('dd').format(jadwal.tanggalPenyaluran!),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMM', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
] else
|
||||
Icon(
|
||||
Icons.event,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
jadwal.nama ?? 'Penyaluran Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (jadwal.deskripsi != null && jadwal.deskripsi!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
jadwal.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
409
lib/app/modules/donatur/views/donatur_laporan_view.dart
Normal file
409
lib/app/modules/donatur/views/donatur_laporan_view.dart
Normal file
@ -0,0 +1,409 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturLaporanView extends GetView<DonaturDashboardController> {
|
||||
const DonaturLaporanView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchLaporanPenyaluran();
|
||||
},
|
||||
child: controller.laporanPenyaluran.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildLaporanList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Laporan Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Laporan penyaluran bantuan belum tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchLaporanPenyaluran(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanList() {
|
||||
// Urutkan laporan berdasarkan tanggal, yang terbaru di atas
|
||||
final sortedLaporan = controller.laporanPenyaluran.toList()
|
||||
..sort((a, b) {
|
||||
if (a.tanggalLaporan == null || b.tanggalLaporan == null) {
|
||||
return 0;
|
||||
}
|
||||
return b.tanggalLaporan!.compareTo(a.tanggalLaporan!);
|
||||
});
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Laporan Penyaluran Bantuan'),
|
||||
Text(
|
||||
'Daftar laporan hasil penyaluran bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...sortedLaporan.map((laporan) => _buildLaporanCard(laporan)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanCard(dynamic laporan) {
|
||||
final formattedDate = laporan.tanggalLaporan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(laporan.tanggalLaporan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: laporan.dokumentasiUrl != null &&
|
||||
laporan.dokumentasiUrl!.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
laporan.dokumentasiUrl!,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.assignment,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.assignment,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
laporan.judul,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
if (laporan.beritaAcaraUrl != null &&
|
||||
laporan.beritaAcaraUrl!.isNotEmpty)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Implementasi untuk membuka berita acara
|
||||
_openDocument(laporan.beritaAcaraUrl!);
|
||||
},
|
||||
icon: const Icon(Icons.description, size: 16),
|
||||
label: const Text('Berita Acara'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minimumSize: const Size(30, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (laporan.dokumentasiUrl != null &&
|
||||
laporan.dokumentasiUrl!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Implementasi untuk melihat dokumentasi
|
||||
_viewDocumentation(laporan.dokumentasiUrl!);
|
||||
},
|
||||
icon: const Icon(Icons.photo_library, size: 16),
|
||||
label: const Text('Dokumentasi'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minimumSize: const Size(30, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openDocument(String url) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Buka Dokumen',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Anda akan membuka dokumen berita acara. Lanjutkan?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// Implementasi untuk membuka URL dokumen
|
||||
// Misalnya menggunakan package url_launcher
|
||||
// launch(url);
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Membuka dokumen: $url',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
child: const Text('Buka'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _viewDocumentation(String url) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: Get.height * 0.6,
|
||||
maxWidth: Get.width,
|
||||
),
|
||||
child: Image.network(
|
||||
url,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 50,
|
||||
color: Colors.red.shade300,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Gagal memuat gambar'),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
1150
lib/app/modules/donatur/views/donatur_penitipan_view.dart
Normal file
1150
lib/app/modules/donatur/views/donatur_penitipan_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
|
||||
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
DonaturRiwayatPenitipanView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Riwayat Penitipan'),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Menunggu'),
|
||||
Tab(text: 'Diterima'),
|
||||
Tab(text: 'Ditolak'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
// Tab Menunggu
|
||||
_buildPenitipanList(context, 'MENUNGGU'),
|
||||
// Tab Diterima
|
||||
_buildPenitipanList(context, 'DITERIMA'),
|
||||
// Tab Ditolak
|
||||
_buildPenitipanList(context, 'DITOLAK'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanList(BuildContext context, String status) {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Filter penitipan berdasarkan status
|
||||
var filteredList = controller.penitipanBantuan
|
||||
.where((item) => item.status == status)
|
||||
.toList();
|
||||
|
||||
// Filter berdasarkan pencarian
|
||||
final searchText = searchController.text.toLowerCase();
|
||||
if (searchText.isNotEmpty) {
|
||||
filteredList = filteredList.where((item) {
|
||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||
final tanggal = item.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(item.tanggalPenitipan!)
|
||||
.toLowerCase()
|
||||
: '';
|
||||
|
||||
return kategoriNama.contains(searchText) ||
|
||||
deskripsi.contains(searchText) ||
|
||||
tanggal.contains(searchText);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchPenitipanBantuan();
|
||||
},
|
||||
child: filteredList.isEmpty
|
||||
? _buildEmptyState(status)
|
||||
: _buildContentList(context, filteredList, status),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(String status) {
|
||||
String statusText = '';
|
||||
switch (status) {
|
||||
case 'MENUNGGU':
|
||||
statusText = 'menunggu verifikasi';
|
||||
break;
|
||||
case 'DITERIMA':
|
||||
statusText = 'diterima';
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusText = 'ditolak';
|
||||
break;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada penitipan $statusText',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Anda belum memiliki riwayat penitipan yang $statusText',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentList(
|
||||
BuildContext context, List<dynamic> filteredList, String status) {
|
||||
Color statusColor;
|
||||
switch (status) {
|
||||
case 'DITERIMA':
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusColor = Colors.orange;
|
||||
break;
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Search field
|
||||
TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari riwayat penitipan...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// Trigger update dengan GetX
|
||||
controller.update();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Info jumlah item
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Daftar Penitipan ${status.toLowerCase()}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${filteredList.length} item',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Daftar penitipan
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildPenitipanCard(
|
||||
context, filteredList[index], statusColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPenitipanCard(
|
||||
BuildContext context, dynamic penitipan, Color statusColor) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
IconData statusIcon;
|
||||
|
||||
switch (penitipan.status) {
|
||||
case 'DITERIMA':
|
||||
statusIcon = Icons.check_circle;
|
||||
break;
|
||||
case 'DITOLAK':
|
||||
statusIcon = Icons.cancel;
|
||||
break;
|
||||
case 'MENUNGGU':
|
||||
default:
|
||||
statusIcon = Icons.hourglass_empty;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
statusIcon,
|
||||
color: statusColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
penitipan.kategoriBantuan?.nama ?? 'Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
penitipan.status ?? 'MENUNGGU',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Jumlah: ${penitipan.jumlah ?? 0}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (penitipan.deskripsi != null &&
|
||||
penitipan.deskripsi!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
penitipan.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (penitipan.status == 'DITOLAK' &&
|
||||
penitipan.alasanPenolakan != null &&
|
||||
penitipan.alasanPenolakan!.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Alasan Penolakan:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
penitipan.alasanPenolakan!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
274
lib/app/modules/donatur/views/donatur_skema_view.dart
Normal file
274
lib/app/modules/donatur/views/donatur_skema_view.dart
Normal file
@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
||||
const DonaturSkemaView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchSkemaBantuan();
|
||||
},
|
||||
child: controller.skemaBantuan.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildSkemaList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum Ada Skema Bantuan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Skema bantuan belum tersedia saat ini',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.fetchSkemaBantuan(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Muat Ulang'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.blue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkemaList() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const SectionHeader(title: 'Skema Bantuan Tersedia'),
|
||||
Text(
|
||||
'Daftar skema bantuan yang dapat Anda titipkan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...controller.skemaBantuan.map((skema) => _buildSkemaCard(skema)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkemaCard(dynamic skema) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.volunteer_activism,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
skema.nama ?? 'Skema Bantuan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (skema.kuota != null)
|
||||
_buildInfoChip(
|
||||
icon: Icons.people_outline,
|
||||
label: 'Kuota: ${skema.kuota} penerima',
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
if (skema.jumlahDiterimaPerOrang != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _buildInfoChip(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
label:
|
||||
'Jumlah per orang: ${skema.jumlahDiterimaPerOrang}',
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (skema.deskripsi != null && skema.deskripsi!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Deskripsi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
skema.deskripsi!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
],
|
||||
),
|
||||
if (skema.kriteria != null && skema.kriteria!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Kriteria Penerima',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
skema.kriteria!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Navigasi ke formulir penitipan bantuan
|
||||
controller.activeTabIndex.value = 3;
|
||||
},
|
||||
icon: const Icon(Icons.add_box_outlined, size: 18),
|
||||
label: const Text('Titipkan Bantuan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green.shade600,
|
||||
minimumSize: const Size(double.infinity, 45),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
326
lib/app/modules/donatur/views/donatur_view.dart
Normal file
326
lib/app/modules/donatur/views/donatur_view.dart
Normal file
@ -0,0 +1,326 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_dashboard_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_skema_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_jadwal_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_laporan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_penitipan_view.dart';
|
||||
import 'package:penyaluran_app/app/widgets/app_bottom_navigation_bar.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class DonaturView extends GetView<DonaturDashboardController> {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
DonaturView({super.key});
|
||||
|
||||
// Override untuk mendapatkan controller dengan tag
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
if (!Get.isRegistered<DonaturDashboardController>(
|
||||
tag: 'donatur_dashboard')) {
|
||||
return Get.put(DonaturDashboardController(),
|
||||
tag: 'donatur_dashboard', permanent: true);
|
||||
}
|
||||
return Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Obx(() {
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const Text('Dashboard Donatur');
|
||||
case 1:
|
||||
return const Text('Skema Bantuan');
|
||||
case 2:
|
||||
return const Text('Jadwal Penyaluran');
|
||||
case 3:
|
||||
return const Text('Penitipan Bantuan');
|
||||
case 4:
|
||||
return const Text('Laporan Penyaluran');
|
||||
default:
|
||||
return const Text('Dashboard Donatur');
|
||||
}
|
||||
}),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
scaffoldKey.currentState?.openDrawer();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
// Tombol riwayat penitipan khusus untuk tab penitipan bantuan
|
||||
Obx(() => controller.activeTabIndex.value == 3
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.history),
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman riwayat penitipan
|
||||
Get.to(
|
||||
() => DonaturRiwayatPenitipanView(),
|
||||
transition: Transition.rightToLeft,
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
// Tombol notifikasi
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {
|
||||
// Navigasi ke halaman notifikasi
|
||||
Get.toNamed('/notifikasi');
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
if (controller.jumlahNotifikasiBelumDibaca.value > 0) {
|
||||
return Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 16,
|
||||
minHeight: 16,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahNotifikasiBelumDibaca.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: _buildDrawer(context),
|
||||
body: Obx(() {
|
||||
// Tampilkan sesuai dengan tab yang aktif
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const DonaturDashboardView();
|
||||
case 1:
|
||||
return const DonaturSkemaView();
|
||||
case 2:
|
||||
return const DonaturJadwalView();
|
||||
case 3:
|
||||
return const DonaturPenitipanView();
|
||||
case 4:
|
||||
return const DonaturLaporanView();
|
||||
default:
|
||||
return const DonaturDashboardView();
|
||||
}
|
||||
}),
|
||||
bottomNavigationBar: Obx(() => AppBottomNavigationBar(
|
||||
currentIndex: controller.activeTabIndex.value,
|
||||
onTap: (index) {
|
||||
controller.activeTabIndex.value = index;
|
||||
},
|
||||
items: [
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
activeIcon: Icons.dashboard,
|
||||
label: 'Dashboard',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.description_outlined,
|
||||
activeIcon: Icons.description,
|
||||
label: 'Skema',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
activeIcon: Icons.calendar_today,
|
||||
label: 'Jadwal',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.add_box_outlined,
|
||||
activeIcon: Icons.add_box,
|
||||
label: 'Penitipan',
|
||||
),
|
||||
AppBottomNavigationBarItem(
|
||||
icon: Icons.assignment_outlined,
|
||||
activeIcon: Icons.assignment,
|
||||
label: 'Laporan',
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawer(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 24,
|
||||
left: 16,
|
||||
right: 16),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white70,
|
||||
backgroundImage: controller.profilePhotoUrl != null &&
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Halo,',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.nama,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'Donatur',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.desa ?? 'Tidak ada desa',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text('Riwayat Donasi'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi riwayat donasi
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Pengaturan'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi pengaturan
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@ import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_peny
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/views/donatur_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
@ -159,17 +161,17 @@ class AppPages {
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/detail',
|
||||
name: '${_Paths.laporanPenyaluran}/detail',
|
||||
page: () => const LaporanPenyaluranDetailView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/create',
|
||||
name: '${_Paths.laporanPenyaluran}/create',
|
||||
page: () => const LaporanPenyaluranCreateView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/edit',
|
||||
name: '${_Paths.laporanPenyaluran}/edit',
|
||||
page: () => const LaporanPenyaluranEditView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
@ -186,5 +188,50 @@ class AppPages {
|
||||
),
|
||||
binding: PenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.donaturDashboard,
|
||||
page: () => DonaturView(),
|
||||
binding: DonaturBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.donaturSkema,
|
||||
page: () {
|
||||
final controller =
|
||||
Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
controller.activeTabIndex.value = 1;
|
||||
return DonaturView();
|
||||
},
|
||||
binding: DonaturBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.donaturJadwal,
|
||||
page: () {
|
||||
final controller =
|
||||
Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
controller.activeTabIndex.value = 2;
|
||||
return DonaturView();
|
||||
},
|
||||
binding: DonaturBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.donaturPenitipan,
|
||||
page: () {
|
||||
final controller =
|
||||
Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
controller.activeTabIndex.value = 3;
|
||||
return DonaturView();
|
||||
},
|
||||
binding: DonaturBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.donaturLaporan,
|
||||
page: () {
|
||||
final controller =
|
||||
Get.find<DonaturDashboardController>(tag: 'donatur_dashboard');
|
||||
controller.activeTabIndex.value = 4;
|
||||
return DonaturView();
|
||||
},
|
||||
binding: DonaturBinding(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ abstract class Routes {
|
||||
static const petugasVerifikasiDashboard = _Paths.petugasVerifikasiDashboard;
|
||||
static const petugasDesaDashboard = _Paths.petugasDesaDashboard;
|
||||
static const donaturDashboard = _Paths.donaturDashboard;
|
||||
static const donaturSkema = _Paths.donaturSkema;
|
||||
static const donaturJadwal = _Paths.donaturJadwal;
|
||||
static const donaturPenitipan = _Paths.donaturPenitipan;
|
||||
static const donaturLaporan = _Paths.donaturLaporan;
|
||||
static const splash = _Paths.splash;
|
||||
static const permintaanPenjadwalan = _Paths.permintaanPenjadwalan;
|
||||
static const daftarPenerima = _Paths.daftarPenerima;
|
||||
@ -51,6 +55,10 @@ abstract class _Paths {
|
||||
static const petugasVerifikasiDashboard = '/petugas-verifikasi-dashboard';
|
||||
static const petugasDesaDashboard = '/petugas-desa-dashboard';
|
||||
static const donaturDashboard = '/donatur-dashboard';
|
||||
static const donaturSkema = '/donatur-skema';
|
||||
static const donaturJadwal = '/donatur-jadwal';
|
||||
static const donaturPenitipan = '/donatur-penitipan';
|
||||
static const donaturLaporan = '/donatur-laporan';
|
||||
static const splash = '/splash';
|
||||
static const permintaanPenjadwalan = '/permintaan-penjadwalan';
|
||||
static const daftarPenerima = '/daftar-penerima';
|
||||
|
Reference in New Issue
Block a user