h-1 lebaran
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
|||||||
M
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>Õ<EFBFBD><EFBFBD>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>
|
<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
|
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
|
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
|
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
|
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 ~
|
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
|
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>
|
||||||
<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
|
<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
|
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
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ȕ<EFBFBD><EFBFBD>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>
|
<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>
|
||||||
<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
|
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
|
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
|
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|
|
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>
|
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
|
|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>
|
||||||
<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
|
<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
|
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
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>͕<EFBFBD><EFBFBD>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
|
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
|
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
|
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
|
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
|
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
|
K <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||||
r
|
r
|
||||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 x
|
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 x
|
||||||
v
|
v
|
||||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||||
~
|
~
|
||||||
|
|
|
|
||||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||||
o
|
o
|
||||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
|||||||
M
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ϕ<EFBFBD><EFBFBD>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
|
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
|
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
|
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
|
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
|
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 Ұ<><D2B0><EFBFBD>2 {
|
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json Ұ<><D2B0><EFBFBD>2 {
|
||||||
y
|
y
|
||||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin Ұ<><D2B0><EFBFBD>2
|
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin Ұ<><D2B0><EFBFBD>2
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
|
|
||||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt Ұ<><D2B0><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt Ұ<><D2B0><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 Ұ<><D2B0><EFBFBD>2
|
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json Ұ<><D2B0><EFBFBD>2
|
||||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
@ -6,7 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="penyaluran_app"
|
android:label="DisalurKita"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@ -47,5 +47,24 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<!-- Konfigurasi untuk url_launcher -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:mimeType="application/pdf" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
BIN
assets/images/logo-disalurkita.png
Normal file
After Width: | Height: | Size: 42 KiB |
108
assets/sql/README.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Fungsi SQL untuk Supabase
|
||||||
|
|
||||||
|
File-file SQL di direktori ini berisi fungsi-fungsi yang perlu dijalankan pada database Supabase untuk mendukung fitur-fitur aplikasi.
|
||||||
|
|
||||||
|
## Cara Menginstal Fungsi SQL
|
||||||
|
|
||||||
|
1. Login ke dashboard Supabase untuk project Anda
|
||||||
|
2. Buka bagian "SQL Editor"
|
||||||
|
3. Klik "New Query"
|
||||||
|
4. Copy dan paste isi file SQL yang ingin diinstal (misalnya `batch_update_jadwal_status.sql`)
|
||||||
|
5. Jalankan query dengan mengklik tombol "Run"
|
||||||
|
|
||||||
|
## Daftar Fungsi SQL
|
||||||
|
|
||||||
|
### batch_update_jadwal_status
|
||||||
|
|
||||||
|
Fungsi ini digunakan untuk mengupdate status banyak jadwal sekaligus, yang lebih efisien daripada melakukan update satu per satu.
|
||||||
|
|
||||||
|
**Sintaks Penggunaan:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT batch_update_jadwal_status(
|
||||||
|
ARRAY[
|
||||||
|
'{"id": "jadwal-id-1", "status": "AKTIF"}',
|
||||||
|
'{"id": "jadwal-id-2", "status": "BATALTERLAKSANA"}'
|
||||||
|
]::jsonb[],
|
||||||
|
'2023-01-01T00:00:00Z'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
- `jadwal_updates`: Array dari objek JSON dengan properti `id` dan `status`
|
||||||
|
- `updated_timestamp` (opsional): Waktu update dalam format ISO 8601
|
||||||
|
|
||||||
|
**Status yang Valid:**
|
||||||
|
Berikut adalah nilai-nilai yang valid untuk kolom status (enum `StatusPenyaluranBantuan`):
|
||||||
|
|
||||||
|
- `DIJADWALKAN` - Jadwal telah dibuat tapi belum aktif
|
||||||
|
- `AKTIF` - Jadwal sedang berlangsung
|
||||||
|
- `TERLAKSANA` - Jadwal telah berhasil dilaksanakan
|
||||||
|
- `BATALTERLAKSANA` - Jadwal tidak terlaksana atau dibatalkan
|
||||||
|
|
||||||
|
**Contoh Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"updated_count": 2,
|
||||||
|
"success_ids": ["jadwal-id-1", "jadwal-id-2"],
|
||||||
|
"timestamp": "2023-01-01T00:00:00Z",
|
||||||
|
"errors": {
|
||||||
|
"count": 0,
|
||||||
|
"ids": [],
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cara Menguji Fungsi
|
||||||
|
|
||||||
|
Setelah fungsi diinstal, Anda dapat mengujinya dengan menjalankan query berikut pada SQL Editor:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Pastikan status yang digunakan sesuai dengan enum StatusPenyaluranBantuan
|
||||||
|
SELECT batch_update_jadwal_status(
|
||||||
|
ARRAY[
|
||||||
|
'{"id": "534cb328-1fd9-4945-8642-c99b8e1acb2d", "status": "DIJADWALKAN"}'
|
||||||
|
]::jsonb[]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Ganti `534cb328-1fd9-4945-8642-c99b8e1acb2d` dengan ID jadwal yang valid dari tabel `penyaluran_bantuan` Anda.
|
||||||
|
|
||||||
|
## Membuat Enum di Database (Jika Belum Ada)
|
||||||
|
|
||||||
|
Jika enum `StatusPenyaluranBantuan` belum ada di database, Anda dapat membuatnya dengan query berikut:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TYPE "StatusPenyaluranBantuan" AS ENUM (
|
||||||
|
'DIJADWALKAN',
|
||||||
|
'AKTIF',
|
||||||
|
'TERLAKSANA',
|
||||||
|
'BATALTERLAKSANA'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Jika muncul error:
|
||||||
|
|
||||||
|
- Periksa apakah ID jadwal valid dan ada di tabel `penyaluran_bantuan`
|
||||||
|
- Pastikan format UUID benar (harus berupa UUID valid, bukan string biasa)
|
||||||
|
- Periksa apakah nilai status valid dan sesuai dengan tipe enum `StatusPenyaluranBantuan`
|
||||||
|
- Pastikan tabel `penyaluran_bantuan` memiliki kolom `status` dengan tipe data enum `StatusPenyaluranBantuan` dan kolom `updated_at`
|
||||||
|
|
||||||
|
Error umum:
|
||||||
|
|
||||||
|
1. `column "status" is of type "StatusPenyaluranBantuan" but expression is of type text` - Ini terjadi karena kolom status memiliki tipe enum, bukan teks biasa. Fungsi sudah menyertakan cast ke enum.
|
||||||
|
2. `operator does not exist: uuid = text` - Ini terjadi jika ID tidak dikonversi ke UUID. Fungsi sudah menyertakan cast ke UUID.
|
||||||
|
|
||||||
|
## Menambahkan Nilai Baru ke Enum
|
||||||
|
|
||||||
|
Jika perlu menambahkan nilai enum baru di masa depan, gunakan SQL berikut:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TYPE "StatusPenyaluranBantuan" ADD VALUE 'NILAI_BARU';
|
||||||
|
```
|
80
assets/sql/batch_update_jadwal_status.sql
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
-- Fungsi untuk memperbarui status banyak jadwal sekaligus
|
||||||
|
-- Penggunaan:
|
||||||
|
-- SELECT batch_update_jadwal_status(
|
||||||
|
-- ARRAY[
|
||||||
|
-- '{"id": "jadwal-id-1", "status": "AKTIF"}',
|
||||||
|
-- '{"id": "jadwal-id-2", "status": "BATALTERLAKSANA"}'
|
||||||
|
-- ]::jsonb[],
|
||||||
|
-- '2023-01-01T00:00:00Z'
|
||||||
|
-- );
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.batch_update_jadwal_status(
|
||||||
|
jadwal_updates jsonb[],
|
||||||
|
updated_timestamp text DEFAULT NOW()
|
||||||
|
) RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
jadwal_item jsonb;
|
||||||
|
jadwal_id text;
|
||||||
|
new_status text;
|
||||||
|
updated_count int := 0;
|
||||||
|
success_ids text[] := '{}';
|
||||||
|
result jsonb;
|
||||||
|
error_ids text[] := '{}';
|
||||||
|
error_messages text[] := '{}';
|
||||||
|
BEGIN
|
||||||
|
-- Loop melalui setiap item dalam array
|
||||||
|
FOREACH jadwal_item IN ARRAY jadwal_updates
|
||||||
|
LOOP
|
||||||
|
-- Ekstrak ID dan status dari item JSON
|
||||||
|
jadwal_id := jadwal_item->>'id';
|
||||||
|
new_status := jadwal_item->>'status';
|
||||||
|
|
||||||
|
-- Konversi ID string ke UUID secara eksplisit dan status ke enum
|
||||||
|
BEGIN
|
||||||
|
-- Update jadwal penyaluran dengan cast eksplisit ke UUID dan StatusPenyaluranBantuan
|
||||||
|
UPDATE public.penyaluran_bantuan
|
||||||
|
SET
|
||||||
|
status = new_status::public."StatusPenyaluranBantuan",
|
||||||
|
updated_at = updated_timestamp
|
||||||
|
WHERE id = jadwal_id::uuid;
|
||||||
|
|
||||||
|
-- Jika berhasil diperbarui
|
||||||
|
IF FOUND THEN
|
||||||
|
updated_count := updated_count + 1;
|
||||||
|
success_ids := array_append(success_ids, jadwal_id);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN invalid_text_representation THEN
|
||||||
|
-- Log error jika konversi UUID gagal
|
||||||
|
RAISE NOTICE 'Invalid UUID format: %', jadwal_id;
|
||||||
|
error_ids := array_append(error_ids, jadwal_id);
|
||||||
|
error_messages := array_append(error_messages, 'Invalid UUID format');
|
||||||
|
WHEN others THEN
|
||||||
|
-- Tangkap error lainnya
|
||||||
|
RAISE NOTICE 'Error updating status for jadwal ID %: %', jadwal_id, SQLERRM;
|
||||||
|
error_ids := array_append(error_ids, jadwal_id);
|
||||||
|
error_messages := array_append(error_messages, SQLERRM);
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Buat hasil dalam format JSON
|
||||||
|
result := jsonb_build_object(
|
||||||
|
'success', updated_count > 0,
|
||||||
|
'updated_count', updated_count,
|
||||||
|
'success_ids', success_ids,
|
||||||
|
'timestamp', updated_timestamp,
|
||||||
|
'errors', jsonb_build_object(
|
||||||
|
'count', array_length(error_ids, 1),
|
||||||
|
'ids', error_ids,
|
||||||
|
'messages', error_messages
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$;
|
@ -51,5 +51,11 @@
|
|||||||
<string>Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima</string>
|
<string>Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Aplikasi memerlukan akses mikrofon untuk merekam video</string>
|
<string>Aplikasi memerlukan akses mikrofon untuk merekam video</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>http</string>
|
||||||
|
<string>https</string>
|
||||||
|
<string>file</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -12,6 +12,7 @@ class LokasiPenyaluranModel {
|
|||||||
final double? latitude;
|
final double? latitude;
|
||||||
final double? longitude;
|
final double? longitude;
|
||||||
final String? petugasDesaId; // Referensi ke PetugasDesa
|
final String? petugasDesaId; // Referensi ke PetugasDesa
|
||||||
|
final bool isLokasiTitip; // Field baru untuk menentukan lokasi penitipan
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ class LokasiPenyaluranModel {
|
|||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
this.petugasDesaId,
|
this.petugasDesaId,
|
||||||
|
this.isLokasiTitip = false, // Nilai default false
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
});
|
});
|
||||||
@ -49,6 +51,7 @@ class LokasiPenyaluranModel {
|
|||||||
latitude: json["latitude"]?.toDouble(),
|
latitude: json["latitude"]?.toDouble(),
|
||||||
longitude: json["longitude"]?.toDouble(),
|
longitude: json["longitude"]?.toDouble(),
|
||||||
petugasDesaId: json["petugas_desa_id"],
|
petugasDesaId: json["petugas_desa_id"],
|
||||||
|
isLokasiTitip: json["is_lokasi_titip"] ?? false,
|
||||||
createdAt: DateTime.parse(json["created_at"]),
|
createdAt: DateTime.parse(json["created_at"]),
|
||||||
updatedAt: json["updated_at"] == null
|
updatedAt: json["updated_at"] == null
|
||||||
? null
|
? null
|
||||||
@ -67,6 +70,7 @@ class LokasiPenyaluranModel {
|
|||||||
"latitude": latitude,
|
"latitude": latitude,
|
||||||
"longitude": longitude,
|
"longitude": longitude,
|
||||||
"petugas_desa_id": petugasDesaId,
|
"petugas_desa_id": petugasDesaId,
|
||||||
|
"is_lokasi_titip": isLokasiTitip,
|
||||||
"created_at": createdAt.toIso8601String(),
|
"created_at": createdAt.toIso8601String(),
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
@ -71,6 +71,14 @@ class PenyaluranBantuanModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mendapatkan foto petugas dari relasi petugas
|
||||||
|
String? get fotoPetugas {
|
||||||
|
if (petugas != null && petugas!['foto_profil'] != null) {
|
||||||
|
return petugas!['foto_profil'];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
factory PenyaluranBantuanModel.fromRawJson(String str) =>
|
factory PenyaluranBantuanModel.fromRawJson(String str) =>
|
||||||
PenyaluranBantuanModel.fromJson(json.decode(str));
|
PenyaluranBantuanModel.fromJson(json.decode(str));
|
||||||
|
|
||||||
@ -126,4 +134,49 @@ class PenyaluranBantuanModel {
|
|||||||
"created_at": createdAt?.toUtc().toIso8601String(),
|
"created_at": createdAt?.toUtc().toIso8601String(),
|
||||||
"updated_at": updatedAt?.toUtc().toIso8601String(),
|
"updated_at": updatedAt?.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Metode copyWith untuk membuat salinan objek dengan perubahan tertentu
|
||||||
|
PenyaluranBantuanModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? nama,
|
||||||
|
String? deskripsi,
|
||||||
|
String? petugasId,
|
||||||
|
String? skemaId,
|
||||||
|
String? lokasiPenyaluranId,
|
||||||
|
String? kategoriBantuanId,
|
||||||
|
int? jumlahPenerima,
|
||||||
|
DateTime? tanggalPenyaluran,
|
||||||
|
String? status,
|
||||||
|
String? alasanPembatalan,
|
||||||
|
DateTime? tanggalPembatalan,
|
||||||
|
DateTime? tanggalSelesai,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
Map<String, dynamic>? lokasiPenyaluran,
|
||||||
|
Map<String, dynamic>? kategori,
|
||||||
|
Map<String, dynamic>? petugas,
|
||||||
|
int? jumlahBantuan,
|
||||||
|
}) {
|
||||||
|
return PenyaluranBantuanModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
nama: nama ?? this.nama,
|
||||||
|
deskripsi: deskripsi ?? this.deskripsi,
|
||||||
|
petugasId: petugasId ?? this.petugasId,
|
||||||
|
skemaId: skemaId ?? this.skemaId,
|
||||||
|
lokasiPenyaluranId: lokasiPenyaluranId ?? this.lokasiPenyaluranId,
|
||||||
|
kategoriBantuanId: kategoriBantuanId ?? this.kategoriBantuanId,
|
||||||
|
jumlahPenerima: jumlahPenerima ?? this.jumlahPenerima,
|
||||||
|
tanggalPenyaluran: tanggalPenyaluran ?? this.tanggalPenyaluran,
|
||||||
|
status: status ?? this.status,
|
||||||
|
alasanPembatalan: alasanPembatalan ?? this.alasanPembatalan,
|
||||||
|
tanggalPembatalan: tanggalPembatalan ?? this.tanggalPembatalan,
|
||||||
|
tanggalSelesai: tanggalSelesai ?? this.tanggalSelesai,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
lokasiPenyaluran: lokasiPenyaluran ?? this.lokasiPenyaluran,
|
||||||
|
kategori: kategori ?? this.kategori,
|
||||||
|
petugas: petugas ?? this.petugas,
|
||||||
|
jumlahBantuan: jumlahBantuan ?? this.jumlahBantuan,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -457,4 +457,12 @@ class AuthProvider {
|
|||||||
Future<void> markNotificationAsRead(int notificationId) async {
|
Future<void> markNotificationAsRead(int notificationId) async {
|
||||||
await _supabaseService.markNotificationAsRead(notificationId);
|
await _supabaseService.markNotificationAsRead(notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk reset password
|
||||||
|
Future<void> resetPasswordForEmail(String email, {String? redirectTo}) async {
|
||||||
|
await _supabaseService.client.auth.resetPasswordForEmail(
|
||||||
|
email,
|
||||||
|
redirectTo: redirectTo,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
8
lib/app/modules/about/bindings/about_binding.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class AboutBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Tidak perlu controller khusus untuk halaman Tentang Kami
|
||||||
|
}
|
||||||
|
}
|
454
lib/app/modules/about/views/about_view.dart
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
|
||||||
|
class AboutView extends StatelessWidget {
|
||||||
|
const AboutView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Tentang Kami'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header dengan logo dan nama brand
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: AppTheme.primaryGradient,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(30),
|
||||||
|
bottomRight: Radius.circular(30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildVersionInfo(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Konten utama
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
title: 'Tentang DisalurKita',
|
||||||
|
content:
|
||||||
|
'DisalurKita adalah platform penyaluran bantuan digital yang mengedepankan transparansi, akuntabilitas, dan kemudahan pengelolaan bantuan. Aplikasi ini memudahkan koordinasi antara petugas desa, donatur, dan warga dalam proses penyaluran bantuan sosial.',
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.visibility_outlined,
|
||||||
|
title: 'Visi Kami',
|
||||||
|
content:
|
||||||
|
'Menjadi platform terdepan dalam penyaluran bantuan sosial yang transparan, akuntabel, dan berdampak nyata bagi masyarakat Indonesia.',
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.location_on_outlined,
|
||||||
|
title: 'Misi Kami',
|
||||||
|
content:
|
||||||
|
'• Memastikan setiap bantuan diterima oleh yang berhak\n• Meningkatkan transparansi dalam proses penyaluran\n• Memberikan kemudahan akses informasi bagi semua pihak\n• Membangun kepercayaan antara donatur dan penerima bantuan',
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.star_outline,
|
||||||
|
title: 'Nilai-nilai Kami',
|
||||||
|
content:
|
||||||
|
'• Transparansi: Keterbukaan dalam setiap proses\n• Akuntabilitas: Pertanggungjawaban yang jelas\n• Inklusivitas: Melibatkan semua pihak\n• Efisiensi: Penyaluran bantuan tepat sasaran\n• Inovasi: Terus berinovasi untuk solusi terbaik',
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Tim Kami',
|
||||||
|
content:
|
||||||
|
'DisalurKita dikembangkan oleh tim yang berdedikasi untuk menciptakan solusi inovatif dalam penyaluran bantuan sosial di Indonesia. Tim kami terdiri dari para profesional di bidang teknologi dan pengembangan sosial.',
|
||||||
|
),
|
||||||
|
|
||||||
|
// Layanan
|
||||||
|
_buildTeamSection(),
|
||||||
|
|
||||||
|
// Hubungi Kami
|
||||||
|
_buildContactSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVersionInfo() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Versi 1.0.0',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSection({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String content,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTeamSection() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.settings_outlined,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text(
|
||||||
|
'Layanan Kami',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Layanan Grid
|
||||||
|
GridView.count(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
childAspectRatio: 0.8,
|
||||||
|
children: [
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.volunteer_activism_outlined,
|
||||||
|
title: 'Penitipan Bantuan',
|
||||||
|
),
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
title: 'Pengelolaan Stok',
|
||||||
|
),
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.local_shipping_outlined,
|
||||||
|
title: 'Penyaluran Bantuan',
|
||||||
|
),
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.people_outline,
|
||||||
|
title: 'Manajemen Penerima',
|
||||||
|
),
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.assignment_outlined,
|
||||||
|
title: 'Laporan Transparan',
|
||||||
|
),
|
||||||
|
_buildServiceItem(
|
||||||
|
icon: Icons.campaign_outlined,
|
||||||
|
title: 'Pengaduan',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildServiceItem({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContactSection() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.contact_mail_outlined,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text(
|
||||||
|
'Hubungi Kami',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blue.shade50, Colors.white],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildContactItem(
|
||||||
|
icon: Icons.email_outlined,
|
||||||
|
title: 'Email',
|
||||||
|
content: 'info@disalurkita.id',
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildContactItem(
|
||||||
|
icon: Icons.phone_outlined,
|
||||||
|
title: 'Telepon',
|
||||||
|
content: '+62 8123 4567 890',
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildContactItem(
|
||||||
|
icon: Icons.location_on_outlined,
|
||||||
|
title: 'Alamat',
|
||||||
|
content: 'Jl. Transparansi No. 123, Jakarta Pusat, Indonesia',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'© ${DateTime.now().year} DisalurKita. Seluruh hak cipta dilindungi.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContactItem({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String content,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -50,6 +50,10 @@ class AuthController extends GetxController {
|
|||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxBool isWargaProfileComplete = false.obs;
|
final RxBool isWargaProfileComplete = false.obs;
|
||||||
|
|
||||||
|
// Variable untuk mengontrol visibility password
|
||||||
|
final RxBool isPasswordHidden = true.obs;
|
||||||
|
final RxBool isConfirmPasswordHidden = true.obs;
|
||||||
|
|
||||||
// Flag untuk menandai apakah sudah melakukan pengambilan data profil
|
// Flag untuk menandai apakah sudah melakukan pengambilan data profil
|
||||||
final RxBool _hasLoadedProfile = false.obs;
|
final RxBool _hasLoadedProfile = false.obs;
|
||||||
|
|
||||||
@ -376,6 +380,65 @@ class AuthController extends GetxController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk reset password
|
||||||
|
Future<void> resetPassword(String email) async {
|
||||||
|
if (email.isEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Email tidak boleh kosong',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetUtils.isEmail(email)) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Email tidak valid',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Menggunakan Supabase untuk reset password
|
||||||
|
await _authProvider.resetPasswordForEmail(
|
||||||
|
email,
|
||||||
|
redirectTo: 'penyaluranapp://reset-password',
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Instruksi reset password telah dikirim ke email Anda',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kembali ke halaman login
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
Get.offNamed(Routes.login);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saat reset password: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Terjadi kesalahan saat mengirim reset password. Silakan coba lagi nanti.',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Metode untuk refresh data user setelah update profil
|
// Metode untuk refresh data user setelah update profil
|
||||||
Future<void> refreshUserData() async {
|
Future<void> refreshUserData() async {
|
||||||
try {
|
try {
|
||||||
@ -543,4 +606,14 @@ class AuthController extends GetxController {
|
|||||||
noHpController.clear();
|
noHpController.clear();
|
||||||
jenisController.clear();
|
jenisController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk toggle visibility password
|
||||||
|
void togglePasswordVisibility() {
|
||||||
|
isPasswordHidden.value = !isPasswordHidden.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk toggle visibility konfirmasi password
|
||||||
|
void toggleConfirmPasswordVisibility() {
|
||||||
|
isConfirmPasswordHidden.value = !isConfirmPasswordHidden.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
269
lib/app/modules/auth/views/forgot_password_view.dart
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
|
|
||||||
|
class ForgotPasswordView extends GetView<AuthController> {
|
||||||
|
const ForgotPasswordView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TextEditingController emailController = TextEditingController();
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Lupa Password',
|
||||||
|
style: TextStyle(color: Color(0xFF1565C0)),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFFE3F2FD), Colors.white],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
// Logo
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.2),
|
||||||
|
spreadRadius: 5,
|
||||||
|
blurRadius: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.lock_reset,
|
||||||
|
size: 70,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
// Judul
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Reset Password',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Masukkan email Anda untuk menerima instruksi reset password',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Color(0xFF546E7A),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// Email Field
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan email Anda',
|
||||||
|
labelText: 'Email',
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.email, color: Color(0xFF1565C0)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Color(0xFF1565C0), width: 1.5),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.red, width: 1.5),
|
||||||
|
),
|
||||||
|
fillColor: Colors.white,
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
validator: controller.validateEmail,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Reset Button
|
||||||
|
Obx(() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFF1565C0).withOpacity(0.3),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
controller.resetPassword(
|
||||||
|
emailController.text.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
backgroundColor: const Color(0xFF1565C0),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? const SpinKitThreeBounce(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'KIRIM INSTRUKSI RESET',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Kembali ke halaman login
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.offNamed(Routes.login),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Kembali ke Halaman Login',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// Informasi Tambahan
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F8E9),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFAED581), width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFAED581),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Color(0xFF33691E),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Informasi Penting',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF33691E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'Petunjuk reset password akan dikirim ke email Anda. Silakan periksa kotak masuk atau folder spam setelah permintaan reset password.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF424242),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,140 +10,360 @@ class LoginView extends GetView<AuthController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: Container(
|
||||||
child: Padding(
|
decoration: const BoxDecoration(
|
||||||
padding: const EdgeInsets.all(20.0),
|
gradient: LinearGradient(
|
||||||
child: SingleChildScrollView(
|
begin: Alignment.topCenter,
|
||||||
child: Form(
|
end: Alignment.bottomCenter,
|
||||||
key: controller.loginFormKey,
|
colors: [Color(0xFFE3F2FD), Colors.white],
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
),
|
||||||
children: [
|
child: SafeArea(
|
||||||
const SizedBox(height: 50),
|
child: Padding(
|
||||||
// Logo atau Judul
|
padding: const EdgeInsets.all(24.0),
|
||||||
const Center(
|
child: SingleChildScrollView(
|
||||||
child: Text(
|
child: Form(
|
||||||
'Penyaluran App',
|
key: controller.loginFormKey,
|
||||||
style: TextStyle(
|
child: Column(
|
||||||
fontSize: 28,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
color: Colors.blue,
|
// Logo
|
||||||
|
Center(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
const Center(
|
||||||
const Center(
|
child: Text(
|
||||||
child: Text(
|
'Masuk ke akun Anda',
|
||||||
'Masuk ke akun Anda',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 16,
|
||||||
fontSize: 16,
|
color: Color(0xFF546E7A),
|
||||||
color: Colors.grey,
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 50),
|
|
||||||
|
|
||||||
// Email Field
|
// Email Field
|
||||||
TextFormField(
|
Container(
|
||||||
controller: controller.emailController,
|
decoration: BoxDecoration(
|
||||||
keyboardType: TextInputType.emailAddress,
|
color: Colors.white,
|
||||||
decoration: InputDecoration(
|
borderRadius: BorderRadius.circular(15),
|
||||||
labelText: 'Email',
|
boxShadow: [
|
||||||
prefixIcon: const Icon(Icons.email),
|
BoxShadow(
|
||||||
border: OutlineInputBorder(
|
color: Colors.black.withOpacity(0.05),
|
||||||
borderRadius: BorderRadius.circular(10),
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller.emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan email Anda',
|
||||||
|
labelText: 'Email',
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.email, color: Color(0xFF1565C0)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Color(0xFF1565C0), width: 1.5),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.red, width: 1.5),
|
||||||
|
),
|
||||||
|
fillColor: Colors.white,
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
validator: controller.validateEmail,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: controller.validateEmail,
|
const SizedBox(height: 20),
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Password Field
|
// Password Field
|
||||||
TextFormField(
|
Container(
|
||||||
controller: controller.passwordController,
|
decoration: BoxDecoration(
|
||||||
obscureText: true,
|
color: Colors.white,
|
||||||
decoration: InputDecoration(
|
borderRadius: BorderRadius.circular(15),
|
||||||
labelText: 'Password',
|
boxShadow: [
|
||||||
prefixIcon: const Icon(Icons.lock),
|
BoxShadow(
|
||||||
border: OutlineInputBorder(
|
color: Colors.black.withOpacity(0.05),
|
||||||
borderRadius: BorderRadius.circular(10),
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
child: Obx(() => TextFormField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
obscureText: controller.isPasswordHidden.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan password Anda',
|
||||||
|
labelText: 'Password',
|
||||||
|
prefixIcon: const Icon(Icons.lock,
|
||||||
|
color: Color(0xFF1565C0)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Color(0xFF1565C0), width: 1.5),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: Colors.red, width: 1.5),
|
||||||
|
),
|
||||||
|
fillColor: Colors.white,
|
||||||
|
filled: true,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.isPasswordHidden.value =
|
||||||
|
!controller.isPasswordHidden.value;
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
!controller.isPasswordHidden.value
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
color: const Color(0xFF78909C),
|
||||||
|
),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validatePassword,
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
validator: controller.validatePassword,
|
const SizedBox(height: 10),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
// Forgot Password
|
// Forgot Password
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () => Get.toNamed(Routes.forgotPassword),
|
||||||
// Implementasi lupa password
|
style: TextButton.styleFrom(
|
||||||
},
|
foregroundColor: const Color(0xFF1565C0),
|
||||||
child: const Text('Lupa Password?'),
|
),
|
||||||
),
|
child: const Text(
|
||||||
),
|
'Lupa Password?',
|
||||||
const SizedBox(height: 20),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
// Login Button
|
|
||||||
Obx(() => ElevatedButton(
|
|
||||||
onPressed: controller.isLoading.value
|
|
||||||
? null
|
|
||||||
: controller.login,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: controller.isLoading.value
|
),
|
||||||
? const SpinKitThreeBounce(
|
),
|
||||||
color: Colors.white,
|
const SizedBox(height: 20),
|
||||||
size: 24,
|
|
||||||
)
|
// Login Button
|
||||||
: const Text(
|
Obx(() => Container(
|
||||||
'MASUK',
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(15),
|
||||||
fontSize: 16,
|
boxShadow: [
|
||||||
fontWeight: FontWeight.bold,
|
BoxShadow(
|
||||||
|
color: const Color(0xFF1565C0).withOpacity(0.3),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.login,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
backgroundColor: const Color(0xFF1565C0),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? const SpinKitThreeBounce(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'MASUK',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.grey.withOpacity(0.1),
|
||||||
|
Colors.grey.withOpacity(0.5),
|
||||||
|
],
|
||||||
|
begin: Alignment.centerRight,
|
||||||
|
end: Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
'ATAU',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF546E7A),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.grey.withOpacity(0.1),
|
||||||
|
Colors.grey.withOpacity(0.5),
|
||||||
|
],
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Register Donatur Button
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Get.toNamed(Routes.registerDonatur),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
foregroundColor: const Color(0xFF1565C0),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color(0xFF1565C0), width: 1.5),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'DAFTAR SEBAGAI DONATUR',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// Informasi Pendaftaran Warga
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF8E1),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFFFCC80), width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFFFCC80),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Color(0xFFE65100),
|
||||||
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(height: 20),
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
// Divider
|
'Informasi Penting',
|
||||||
const Row(
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 16,
|
||||||
Expanded(child: Divider()),
|
fontWeight: FontWeight.bold,
|
||||||
Padding(
|
color: Color(0xFFE65100),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
),
|
||||||
child:
|
),
|
||||||
Text('ATAU', style: TextStyle(color: Colors.grey)),
|
),
|
||||||
),
|
],
|
||||||
Expanded(child: Divider()),
|
),
|
||||||
],
|
const SizedBox(height: 12),
|
||||||
),
|
const Text(
|
||||||
const SizedBox(height: 20),
|
'Pendaftaran warga hanya dapat dilakukan melalui aplikasi verifikasi data warga. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
|
||||||
|
style: TextStyle(
|
||||||
// Register Donatur Button
|
fontSize: 14,
|
||||||
OutlinedButton(
|
color: Color(0xFF424242),
|
||||||
onPressed: () => Get.toNamed(Routes.registerDonatur),
|
height: 1.5,
|
||||||
style: OutlinedButton.styleFrom(
|
),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
],
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
side: const BorderSide(color: Colors.blue),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'DAFTAR SEBAGAI DONATUR',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 30),
|
||||||
],
|
|
||||||
|
// Footer
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'© ${DateTime.now().year} DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF90A4AE),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -11,8 +11,16 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Daftar Sebagai Donatur'),
|
title: const Text('Daftar Donatur'),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
bottom: Radius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -23,29 +31,60 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 20),
|
|
||||||
// Logo atau Judul
|
|
||||||
const Center(
|
|
||||||
child: Text(
|
|
||||||
'Daftar Donatur',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
const Center(
|
// Header dengan icon dan judul
|
||||||
child: Text(
|
Container(
|
||||||
'Isi data untuk mendaftar sebagai donatur',
|
padding: const EdgeInsets.all(15),
|
||||||
style: TextStyle(
|
decoration: BoxDecoration(
|
||||||
fontSize: 16,
|
color: Colors.blue.shade50,
|
||||||
color: Colors.grey,
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Daftar Sebagai Donatur',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text(
|
||||||
|
'Bergabunglah dengan kami untuk membantu mereka yang membutuhkan',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 20),
|
||||||
|
// Step indicator
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.person_add, color: Colors.blue),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'Informasi Akun',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Nama Lengkap
|
// Nama Lengkap
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -53,9 +92,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
keyboardType: TextInputType.name,
|
keyboardType: TextInputType.name,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nama Lengkap',
|
labelText: 'Nama Lengkap',
|
||||||
prefixIcon: const Icon(Icons.person),
|
hintText: 'Masukkan nama lengkap Anda',
|
||||||
border: OutlineInputBorder(
|
prefixIcon: const Icon(Icons.person, color: Colors.blue),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: controller.validateDonaturNama,
|
validator: controller.validateDonaturNama,
|
||||||
@ -68,9 +120,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
prefixIcon: const Icon(Icons.email),
|
hintText: 'contoh@email.com',
|
||||||
border: OutlineInputBorder(
|
prefixIcon: const Icon(Icons.email, color: Colors.blue),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: controller.validateEmail,
|
validator: controller.validateEmail,
|
||||||
@ -78,34 +143,101 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
TextFormField(
|
Obx(() => TextFormField(
|
||||||
controller: controller.passwordController,
|
controller: controller.passwordController,
|
||||||
obscureText: true,
|
obscureText: controller.isPasswordHidden.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
prefixIcon: const Icon(Icons.lock),
|
hintText: 'Minimal 8 karakter',
|
||||||
border: OutlineInputBorder(
|
prefixIcon:
|
||||||
borderRadius: BorderRadius.circular(10),
|
const Icon(Icons.lock, color: Colors.blue),
|
||||||
),
|
suffixIcon: IconButton(
|
||||||
),
|
icon: Icon(
|
||||||
validator: controller.validatePassword,
|
controller.isPasswordHidden.value
|
||||||
),
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
controller.togglePasswordVisibility(),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validatePassword,
|
||||||
|
)),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
// Confirm Password
|
// Confirm Password
|
||||||
TextFormField(
|
Obx(() => TextFormField(
|
||||||
controller: controller.confirmPasswordController,
|
controller: controller.confirmPasswordController,
|
||||||
obscureText: true,
|
obscureText: controller.isConfirmPasswordHidden.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Konfirmasi Password',
|
labelText: 'Konfirmasi Password',
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
hintText: 'Masukkan password yang sama',
|
||||||
border: OutlineInputBorder(
|
prefixIcon: const Icon(Icons.lock_outline,
|
||||||
borderRadius: BorderRadius.circular(10),
|
color: Colors.blue),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
controller.isConfirmPasswordHidden.value
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
controller.toggleConfirmPasswordVisibility(),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateConfirmPassword,
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Section heading
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.person_pin_circle, color: Colors.blue),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'Informasi Profil',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
validator: controller.validateConfirmPassword,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// No HP
|
// No HP
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -113,9 +245,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nomor HP',
|
labelText: 'Nomor HP',
|
||||||
prefixIcon: const Icon(Icons.phone),
|
hintText: 'Masukkan nomor HP aktif',
|
||||||
border: OutlineInputBorder(
|
prefixIcon: const Icon(Icons.phone, color: Colors.blue),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: controller.validateDonaturNoHp,
|
validator: controller.validateDonaturNoHp,
|
||||||
@ -128,10 +273,23 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
keyboardType: TextInputType.streetAddress,
|
keyboardType: TextInputType.streetAddress,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Alamat',
|
labelText: 'Alamat Lengkap',
|
||||||
prefixIcon: const Icon(Icons.home),
|
hintText: 'Masukkan alamat lengkap Anda',
|
||||||
border: OutlineInputBorder(
|
prefixIcon: const Icon(Icons.home, color: Colors.blue),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: controller.validateDonaturAlamat,
|
validator: controller.validateDonaturAlamat,
|
||||||
@ -139,70 +297,160 @@ class RegisterDonaturView extends GetView<AuthController> {
|
|||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
// Jenis Donatur (Dropdown)
|
// Jenis Donatur (Dropdown)
|
||||||
DropdownButtonFormField<String>(
|
Container(
|
||||||
value: controller.jenisController.text.isEmpty
|
decoration: BoxDecoration(
|
||||||
? 'Individu'
|
color: Colors.grey.shade100,
|
||||||
: controller.jenisController.text,
|
borderRadius: BorderRadius.circular(10),
|
||||||
decoration: InputDecoration(
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
labelText: 'Jenis Donatur',
|
),
|
||||||
prefixIcon: const Icon(Icons.category),
|
child: DropdownButtonFormField<String>(
|
||||||
border: OutlineInputBorder(
|
value: controller.jenisController.text.isEmpty
|
||||||
borderRadius: BorderRadius.circular(10),
|
? 'Individu'
|
||||||
),
|
: controller.jenisController.text,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Jenis Donatur',
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.category, color: Colors.blue),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Individu', child: Text('Individu')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Organisasi', child: Text('Organisasi')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Perusahaan', child: Text('Perusahaan')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Lainnya', child: Text('Lainnya')),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.jenisController.text = value ?? 'Individu';
|
||||||
|
},
|
||||||
),
|
),
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'Individu', child: Text('Individu')),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'Organisasi', child: Text('Organisasi')),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'Perusahaan', child: Text('Perusahaan')),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'Lainnya', child: Text('Lainnya')),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
controller.jenisController.text = value ?? 'Individu';
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
|
||||||
|
|
||||||
// Register Button
|
const SizedBox(height: 25),
|
||||||
Obx(() => ElevatedButton(
|
|
||||||
onPressed: controller.isLoading.value
|
// Catatan Informasi
|
||||||
? null
|
Container(
|
||||||
: controller.registerDonatur,
|
padding: const EdgeInsets.all(15),
|
||||||
style: ElevatedButton.styleFrom(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
color: Colors.blue.shade50,
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderRadius: BorderRadius.circular(10),
|
border: Border.all(color: Colors.blue.shade200),
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
child: controller.isLoading.value
|
children: [
|
||||||
? const SpinKitThreeBounce(
|
const Icon(Icons.info_outline, color: Colors.blue),
|
||||||
color: Colors.white,
|
const SizedBox(width: 10),
|
||||||
size: 24,
|
Expanded(
|
||||||
)
|
child: Column(
|
||||||
: const Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'DAFTAR',
|
children: const [
|
||||||
|
Text(
|
||||||
|
'Informasi',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
'Data Anda akan terverifikasi dan terlindungi. Kami menjaga privasi dan keamanan data Anda.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// Register Button
|
||||||
|
Obx(() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.3),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.registerDonatur,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? const SpinKitThreeBounce(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
)
|
||||||
|
: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.how_to_reg, color: Colors.white),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'DAFTAR SEKARANG',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Login Link
|
// Login Link
|
||||||
Row(
|
Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.all(15),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
const Text('Sudah punya akun?'),
|
color: Colors.grey.shade50,
|
||||||
TextButton(
|
borderRadius: BorderRadius.circular(10),
|
||||||
onPressed: () => Get.offAllNamed(Routes.login),
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
child: const Text('Masuk'),
|
),
|
||||||
),
|
child: Row(
|
||||||
],
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Sudah punya akun?',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.offAllNamed(Routes.login),
|
||||||
|
child: const Text(
|
||||||
|
'Masuk',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -7,6 +7,7 @@ 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/laporan_penyaluran_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/data/models/lokasi_penyaluran_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
@ -45,6 +46,10 @@ class DonaturDashboardController extends GetxController {
|
|||||||
// Data untuk stok bantuan yang tersedia
|
// Data untuk stok bantuan yang tersedia
|
||||||
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
|
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
|
||||||
|
|
||||||
|
// Data untuk lokasi penyaluran
|
||||||
|
final RxList<LokasiPenyaluranModel> lokasiPenyaluran =
|
||||||
|
<LokasiPenyaluranModel>[].obs;
|
||||||
|
|
||||||
// Indikator loading
|
// Indikator loading
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
@ -199,6 +204,9 @@ class DonaturDashboardController extends GetxController {
|
|||||||
// Ambil data stok bantuan
|
// Ambil data stok bantuan
|
||||||
await fetchStokBantuan();
|
await fetchStokBantuan();
|
||||||
|
|
||||||
|
// Ambil data lokasi penyaluran
|
||||||
|
await fetchLokasiPenyaluran();
|
||||||
|
|
||||||
// Ambil data notifikasi
|
// Ambil data notifikasi
|
||||||
await fetchNotifikasi();
|
await fetchNotifikasi();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -233,7 +241,7 @@ class DonaturDashboardController extends GetxController {
|
|||||||
.from('penyaluran_bantuan')
|
.from('penyaluran_bantuan')
|
||||||
.select(
|
.select(
|
||||||
'*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)')
|
'*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)')
|
||||||
.order('tanggal_penyaluran', ascending: true);
|
.order('tanggal_penyaluran', ascending: false);
|
||||||
|
|
||||||
// Konversi ke model lalu filter di sisi client
|
// Konversi ke model lalu filter di sisi client
|
||||||
final allJadwal = response
|
final allJadwal = response
|
||||||
@ -243,9 +251,7 @@ class DonaturDashboardController extends GetxController {
|
|||||||
|
|
||||||
// Filter jadwal yang tanggalnya lebih besar dari hari ini
|
// Filter jadwal yang tanggalnya lebih besar dari hari ini
|
||||||
jadwalPenyaluran.value = allJadwal
|
jadwalPenyaluran.value = allJadwal
|
||||||
.where((jadwal) =>
|
.where((jadwal) => jadwal.tanggalPenyaluran != null)
|
||||||
jadwal.tanggalPenyaluran != null &&
|
|
||||||
jadwal.tanggalPenyaluran!.isAfter(now))
|
|
||||||
.toList();
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error fetching jadwal penyaluran: $e');
|
print('Error fetching jadwal penyaluran: $e');
|
||||||
@ -306,6 +312,23 @@ class DonaturDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ambil data lokasi penyaluran
|
||||||
|
Future<void> fetchLokasiPenyaluran() async {
|
||||||
|
try {
|
||||||
|
final response = await _supabaseService.client
|
||||||
|
.from('lokasi_penyaluran')
|
||||||
|
.select()
|
||||||
|
.eq('is_lokasi_titip', true)
|
||||||
|
.order('nama');
|
||||||
|
|
||||||
|
lokasiPenyaluran.value = (response as List<dynamic>)
|
||||||
|
.map((data) => LokasiPenyaluranModel.fromJson(data))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching lokasi penyaluran: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ambil data notifikasi
|
// Ambil data notifikasi
|
||||||
Future<void> fetchNotifikasi() async {
|
Future<void> fetchNotifikasi() async {
|
||||||
try {
|
try {
|
||||||
@ -386,6 +409,7 @@ class DonaturDashboardController extends GetxController {
|
|||||||
double jumlah,
|
double jumlah,
|
||||||
String deskripsi,
|
String deskripsi,
|
||||||
String? skemaBantuanId,
|
String? skemaBantuanId,
|
||||||
|
String? lokasiPenyaluranId,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
@ -426,15 +450,25 @@ class DonaturDashboardController extends GetxController {
|
|||||||
'tanggal_penitipan': DateTime.now().toIso8601String(),
|
'tanggal_penitipan': DateTime.now().toIso8601String(),
|
||||||
'foto_bantuan': fotoBantuanUrls,
|
'foto_bantuan': fotoBantuanUrls,
|
||||||
'is_uang': selectedStokBantuan.isUang ?? false,
|
'is_uang': selectedStokBantuan.isUang ?? false,
|
||||||
|
'skema_bantuan_id': skemaBantuanId,
|
||||||
|
'lokasi_penyaluran_id': lokasiPenyaluranId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tambahkan skema bantuan jika ada
|
|
||||||
if (skemaBantuanId != null && skemaBantuanId.isNotEmpty) {
|
|
||||||
data['skema_bantuan_id'] = skemaBantuanId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simpan ke database
|
// Simpan ke database
|
||||||
await _supabaseService.client.from('penitipan_bantuan').insert(data);
|
final response = await _supabaseService.client
|
||||||
|
.from('penitipan_bantuan')
|
||||||
|
.insert(data)
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Tampilkan pesan sukses
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil',
|
||||||
|
'Penitipan bantuan berhasil diinput',
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
|
||||||
// Reset foto bantuan setelah berhasil disimpan
|
// Reset foto bantuan setelah berhasil disimpan
|
||||||
resetFotoBantuan();
|
resetFotoBantuan();
|
||||||
@ -442,19 +476,13 @@ class DonaturDashboardController extends GetxController {
|
|||||||
// Ambil data penitipan bantuan yang baru
|
// Ambil data penitipan bantuan yang baru
|
||||||
await fetchPenitipanBantuan();
|
await fetchPenitipanBantuan();
|
||||||
|
|
||||||
// Tampilkan pesan sukses
|
// Kembali ke halaman utama
|
||||||
Get.snackbar(
|
Get.back();
|
||||||
'Berhasil',
|
|
||||||
'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa',
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
duration: const Duration(seconds: 3),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error creating penitipan bantuan: $e');
|
print('Error creating penitipan bantuan: $e');
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Gagal',
|
'Gagal',
|
||||||
'Terjadi kesalahan saat mengirim penitipan bantuan: $e',
|
'Terjadi kesalahan: $e',
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
@ -475,7 +503,7 @@ class DonaturDashboardController extends GetxController {
|
|||||||
.eq('id', lokasiId)
|
.eq('id', lokasiId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (response != null && response['nama'] != null) {
|
if (response['nama'] != null) {
|
||||||
return response['nama'] as String;
|
return response['nama'] as String;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||||
|
|
||||||
class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||||
@ -36,13 +36,57 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Header DisalurKita dengan logo dan slogan
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
_buildWelcomeSection(),
|
_buildWelcomeSection(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildStatisticSection(),
|
_buildStatisticSection(),
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildUpcomingEvents(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildRecentPenitipan(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -101,14 +145,24 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 30,
|
radius: 30,
|
||||||
backgroundColor: Colors.blue.shade100,
|
backgroundColor: Colors.blue.shade100,
|
||||||
backgroundImage: controller.profilePhotoUrl != null
|
backgroundImage: controller.profilePhotoUrl != null &&
|
||||||
|
controller.profilePhotoUrl!.isNotEmpty
|
||||||
? NetworkImage(controller.profilePhotoUrl!)
|
? NetworkImage(controller.profilePhotoUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: controller.profilePhotoUrl == null
|
child: (controller.profilePhotoUrl == null ||
|
||||||
? Icon(
|
controller.profilePhotoUrl!.isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
color: Colors.blue.shade700,
|
controller.nama.isNotEmpty
|
||||||
size: 30,
|
? controller.nama
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -263,7 +317,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
child: _buildStatCard(
|
child: _buildStatCard(
|
||||||
title: 'Diterima',
|
title: 'Diterima',
|
||||||
value:
|
value:
|
||||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}',
|
'${controller.penitipanBantuan.where((p) => p.status == 'TERVERIFIKASI').length}',
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
@ -284,125 +338,6 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
Widget _buildInfoRow({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required Color iconColor,
|
required Color iconColor,
|
||||||
@ -545,7 +480,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
|
|
||||||
Widget _buildEventCard(dynamic event) {
|
Widget _buildEventCard(dynamic event) {
|
||||||
final formattedDate = event.tanggalPenyaluran != null
|
final formattedDate = event.tanggalPenyaluran != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!)
|
? FormatHelper.formatDateTime(event.tanggalPenyaluran!)
|
||||||
: 'Tanggal tidak tersedia';
|
: 'Tanggal tidak tersedia';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@ -588,7 +523,8 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
event.tanggalPenyaluran != null
|
event.tanggalPenyaluran != null
|
||||||
? DateFormat('dd').format(event.tanggalPenyaluran!)
|
? FormatHelper.formatDateTime(
|
||||||
|
event.tanggalPenyaluran!)
|
||||||
: '--',
|
: '--',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@ -640,8 +576,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
|||||||
|
|
||||||
Widget _buildPenitipanCard(dynamic penitipan) {
|
Widget _buildPenitipanCard(dynamic penitipan) {
|
||||||
final formattedDate = penitipan.tanggalPenitipan != null
|
final formattedDate = penitipan.tanggalPenitipan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||||
.format(penitipan.tanggalPenitipan!)
|
|
||||||
: 'Tanggal tidak tersedia';
|
: 'Tanggal tidak tersedia';
|
||||||
|
|
||||||
Color statusColor;
|
Color statusColor;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
|
|
||||||
class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||||
const DonaturJadwalDetailView({Key? key}) : super(key: key);
|
const DonaturJadwalDetailView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DonaturDashboardController get controller {
|
DonaturDashboardController get controller {
|
||||||
@ -35,7 +35,6 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
_buildDetailSection(jadwal),
|
_buildDetailSection(jadwal),
|
||||||
_buildPelaksanaSection(jadwal),
|
_buildPelaksanaSection(jadwal),
|
||||||
_buildStatusSection(jadwal),
|
_buildStatusSection(jadwal),
|
||||||
_buildActionSection(jadwal),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -126,8 +125,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
|
||||||
.format(jadwal.tanggalPenyaluran!),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -204,11 +202,26 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 25,
|
radius: 25,
|
||||||
backgroundColor: Colors.blue.shade100,
|
backgroundColor: Colors.blue.shade100,
|
||||||
child: Icon(
|
backgroundImage: jadwal.fotoPetugas != null &&
|
||||||
Icons.person,
|
jadwal.fotoPetugas.toString().isNotEmpty
|
||||||
color: Colors.blue.shade700,
|
? NetworkImage(jadwal.fotoPetugas as String)
|
||||||
size: 30,
|
: null,
|
||||||
),
|
child: (jadwal.fotoPetugas == null ||
|
||||||
|
jadwal.fotoPetugas.toString().isEmpty)
|
||||||
|
? Text(
|
||||||
|
jadwal.namaPetugas != null
|
||||||
|
? jadwal.namaPetugas
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -254,50 +267,87 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
children: [
|
children: [
|
||||||
const SectionHeader(title: 'Status Penyaluran'),
|
const SectionHeader(title: 'Status Penyaluran'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildStatusTimeline(jadwal),
|
_buildStatusCard(jadwal),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusTimeline(PenyaluranBantuanModel jadwal) {
|
Widget _buildStatusCard(PenyaluranBantuanModel jadwal) {
|
||||||
final status = jadwal.status;
|
final status = jadwal.status;
|
||||||
final bool isCompleted = status == 'SELESAI';
|
final bool isCompleted = status == 'TERLAKSANA';
|
||||||
final bool isCancelled = status == 'DIBATALKAN';
|
final bool isCancelled = status == 'BATALTERLAKSANA';
|
||||||
final bool isInProgress = status == 'DALAM_PROSES';
|
final bool isInProgress = status == 'AKTIF';
|
||||||
|
final bool isScheduled = status == 'Dijadwalkan';
|
||||||
|
|
||||||
|
Color statusColor = Colors.blue;
|
||||||
|
IconData statusIcon = Icons.schedule;
|
||||||
|
String statusText = 'Dijadwalkan';
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
statusColor = Colors.green;
|
||||||
|
statusIcon = Icons.check_circle;
|
||||||
|
statusText = 'Terlaksana';
|
||||||
|
} else if (isCancelled) {
|
||||||
|
statusColor = Colors.red;
|
||||||
|
statusIcon = Icons.cancel;
|
||||||
|
statusText = 'Batal Terlaksana';
|
||||||
|
} else if (isInProgress) {
|
||||||
|
statusColor = Colors.blue;
|
||||||
|
statusIcon = Icons.sync;
|
||||||
|
statusText = 'Aktif';
|
||||||
|
} else if (isScheduled) {
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
statusIcon = Icons.schedule;
|
||||||
|
statusText = 'Dijadwalkan';
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_buildTimelineItem(
|
Container(
|
||||||
title: 'Dijadwalkan',
|
padding: const EdgeInsets.all(16),
|
||||||
date: jadwal.createdAt != null
|
decoration: BoxDecoration(
|
||||||
? DateFormat('dd MMM yyyy', 'id_ID').format(jadwal.createdAt!)
|
color: statusColor.withOpacity(0.1),
|
||||||
: '-',
|
borderRadius: BorderRadius.circular(12),
|
||||||
isCompleted: true,
|
border: Border.all(color: statusColor.withOpacity(0.3)),
|
||||||
isFirst: true,
|
),
|
||||||
),
|
child: Column(
|
||||||
_buildTimelineItem(
|
children: [
|
||||||
title: 'Dalam Proses',
|
Row(
|
||||||
date: isInProgress || isCompleted
|
children: [
|
||||||
? jadwal.tanggalPenyaluran != null
|
Icon(statusIcon, color: statusColor, size: 28),
|
||||||
? DateFormat('dd MMM yyyy', 'id_ID')
|
const SizedBox(width: 12),
|
||||||
.format(jadwal.tanggalPenyaluran!)
|
Text(
|
||||||
: '-'
|
statusText,
|
||||||
: '-',
|
style: TextStyle(
|
||||||
isCompleted: isInProgress || isCompleted,
|
fontSize: 18,
|
||||||
isCancelled: isCancelled,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: statusColor,
|
||||||
_buildTimelineItem(
|
),
|
||||||
title: 'Selesai',
|
),
|
||||||
date: isCompleted
|
],
|
||||||
? jadwal.tanggalSelesai != null
|
),
|
||||||
? DateFormat('dd MMM yyyy', 'id_ID')
|
const SizedBox(height: 16),
|
||||||
.format(jadwal.tanggalSelesai!)
|
_buildStatusDetailItem(
|
||||||
: '-'
|
title: 'Tanggal Dijadwalkan',
|
||||||
: '-',
|
value: FormatHelper.formatDateIndonesian(jadwal.createdAt),
|
||||||
isCompleted: isCompleted,
|
),
|
||||||
isCancelled: isCancelled,
|
const SizedBox(height: 8),
|
||||||
isLast: true,
|
_buildStatusDetailItem(
|
||||||
|
title: 'Tanggal Penyaluran',
|
||||||
|
value:
|
||||||
|
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
|
||||||
|
),
|
||||||
|
if (isCompleted) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildStatusDetailItem(
|
||||||
|
title: 'Tanggal Selesai',
|
||||||
|
value:
|
||||||
|
FormatHelper.formatDateIndonesian(jadwal.tanggalSelesai),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isCancelled) ...[
|
if (isCancelled) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -333,7 +383,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (jadwal.tanggalPembatalan != null)
|
if (jadwal.tanggalPembatalan != null)
|
||||||
Text(
|
Text(
|
||||||
'Dibatalkan pada: ${DateFormat('dd MMMM yyyy', 'id_ID').format(jadwal.tanggalPembatalan!)}',
|
'Dibatalkan pada: ${FormatHelper.formatDateIndonesian(jadwal.tanggalPembatalan)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.red.shade700,
|
color: Colors.red.shade700,
|
||||||
@ -347,159 +397,29 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTimelineItem({
|
Widget _buildStatusDetailItem(
|
||||||
required String title,
|
{required String title, required String value}) {
|
||||||
required String date,
|
|
||||||
required bool isCompleted,
|
|
||||||
bool isFirst = false,
|
|
||||||
bool isLast = false,
|
|
||||||
bool isCancelled = false,
|
|
||||||
}) {
|
|
||||||
return Row(
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Text(
|
||||||
width: 20,
|
title,
|
||||||
child: Column(
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 14,
|
||||||
if (!isFirst)
|
color: Colors.grey.shade700,
|
||||||
Container(
|
|
||||||
width: 2,
|
|
||||||
height: 20,
|
|
||||||
color: isCompleted
|
|
||||||
? Colors.green
|
|
||||||
: isCancelled
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: isCompleted
|
|
||||||
? Colors.green
|
|
||||||
: isCancelled
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
border: Border.all(
|
|
||||||
color: isCompleted
|
|
||||||
? Colors.green
|
|
||||||
: isCancelled
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: isCompleted
|
|
||||||
? const Icon(Icons.check, size: 12, color: Colors.white)
|
|
||||||
: isCancelled
|
|
||||||
? const Icon(Icons.close, size: 12, color: Colors.white)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
if (!isLast)
|
|
||||||
Container(
|
|
||||||
width: 2,
|
|
||||||
height: 20,
|
|
||||||
color: isCompleted && !isCancelled
|
|
||||||
? Colors.green
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
Text(
|
||||||
Expanded(
|
value,
|
||||||
child: Container(
|
style: const TextStyle(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
fontSize: 14,
|
||||||
child: Column(
|
fontWeight: FontWeight.bold,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: isCompleted
|
|
||||||
? Colors.black
|
|
||||||
: isCancelled
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
date,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: isCompleted
|
|
||||||
? Colors.grey.shade700
|
|
||||||
: isCancelled
|
|
||||||
? Colors.red.shade300
|
|
||||||
: Colors.grey.shade400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionSection(PenyaluranBantuanModel jadwal) {
|
|
||||||
if (jadwal.status == 'DIBATALKAN') {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SectionHeader(title: 'Tindakan'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => _hubungiPetugas(jadwal),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 12),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.chat_outlined),
|
|
||||||
label: const Text('Hubungi Petugas'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (jadwal.status == 'SELESAI') ...[
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () => _lihatLaporan(jadwal),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 12),
|
|
||||||
foregroundColor: Colors.blue,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.description_outlined),
|
|
||||||
label: const Text('Lihat Laporan'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInfoItem({
|
Widget _buildInfoItem({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||||
|
|
||||||
class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||||
@ -97,7 +98,7 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
for (var jadwal in controller.jadwalPenyaluran) {
|
for (var jadwal in controller.jadwalPenyaluran) {
|
||||||
if (jadwal.tanggalPenyaluran != null) {
|
if (jadwal.tanggalPenyaluran != null) {
|
||||||
String monthYear =
|
String monthYear =
|
||||||
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!);
|
FormatHelper.formatDate(jadwal.tanggalPenyaluran!, format: 'MMMM');
|
||||||
|
|
||||||
if (!groupedJadwal.containsKey(monthYear)) {
|
if (!groupedJadwal.containsKey(monthYear)) {
|
||||||
groupedJadwal[monthYear] = [];
|
groupedJadwal[monthYear] = [];
|
||||||
@ -110,9 +111,14 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
// Urutkan kunci (bulan) secara kronologis
|
// Urutkan kunci (bulan) secara kronologis
|
||||||
List<String> sortedMonths = groupedJadwal.keys.toList()
|
List<String> sortedMonths = groupedJadwal.keys.toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
try {
|
||||||
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
|
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
||||||
return dateA.compareTo(dateB);
|
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
|
||||||
|
return dateA.compareTo(dateB);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback sorting jika parse error
|
||||||
|
return a.compareTo(b);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
@ -158,28 +164,27 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
|
|
||||||
Widget _buildJadwalCard(dynamic jadwal) {
|
Widget _buildJadwalCard(dynamic jadwal) {
|
||||||
final formattedDate = jadwal.tanggalPenyaluran != null
|
final formattedDate = jadwal.tanggalPenyaluran != null
|
||||||
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(jadwal.tanggalPenyaluran!)
|
||||||
.format(jadwal.tanggalPenyaluran!)
|
|
||||||
: 'Tanggal tidak tersedia';
|
: 'Tanggal tidak tersedia';
|
||||||
|
|
||||||
String statusText = 'Akan Datang';
|
String statusText = 'Dijadwalkan';
|
||||||
Color statusColor = Colors.blue;
|
Color statusColor = Colors.blue;
|
||||||
|
|
||||||
switch (jadwal.status) {
|
switch (jadwal.status) {
|
||||||
case 'SELESAI':
|
case 'TERLAKSANA':
|
||||||
statusText = 'Selesai';
|
statusText = 'Terlaksana';
|
||||||
statusColor = Colors.green;
|
statusColor = Colors.green;
|
||||||
break;
|
break;
|
||||||
case 'DIBATALKAN':
|
case 'BATALTERLAKSANA':
|
||||||
statusText = 'Dibatalkan';
|
statusText = 'Batal Terlaksana';
|
||||||
statusColor = Colors.red;
|
statusColor = Colors.red;
|
||||||
break;
|
break;
|
||||||
case 'DALAM_PROSES':
|
case 'AKTIF':
|
||||||
statusText = 'Dalam Proses';
|
statusText = 'Aktif';
|
||||||
statusColor = Colors.orange;
|
statusColor = Colors.blue;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
statusText = 'Akan Datang';
|
statusText = 'Dijadwalkan';
|
||||||
statusColor = Colors.blue;
|
statusColor = Colors.blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,8 +253,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
jadwal.tanggalPenyaluran != null
|
jadwal.tanggalPenyaluran != null
|
||||||
? DateFormat('MMM', 'id_ID')
|
? FormatHelper.formatDate(
|
||||||
.format(jadwal.tanggalPenyaluran!)
|
jadwal.tanggalPenyaluran!,
|
||||||
|
format: 'MMM')
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
: 'TBD',
|
: 'TBD',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -265,8 +271,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
jadwal.tanggalPenyaluran != null
|
jadwal.tanggalPenyaluran != null
|
||||||
? DateFormat('dd')
|
? FormatHelper.formatDate(
|
||||||
.format(jadwal.tanggalPenyaluran!)
|
jadwal.tanggalPenyaluran!,
|
||||||
|
format: 'dd')
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
@ -459,11 +466,11 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
|||||||
|
|
||||||
IconData _getStatusIcon(String? status) {
|
IconData _getStatusIcon(String? status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'SELESAI':
|
case 'TERLAKSANA':
|
||||||
return Icons.check_circle;
|
return Icons.check_circle;
|
||||||
case 'DIBATALKAN':
|
case 'BATALTERLAKSANA':
|
||||||
return Icons.cancel;
|
return Icons.cancel;
|
||||||
case 'DALAM_PROSES':
|
case 'AKTIF':
|
||||||
return Icons.timelapse;
|
return Icons.timelapse;
|
||||||
default:
|
default:
|
||||||
return Icons.event_available;
|
return Icons.event_available;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||||
DonaturRiwayatPenitipanView({super.key});
|
DonaturRiwayatPenitipanView({super.key});
|
||||||
@ -60,8 +61,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||||
final tanggal = item.tanggalPenitipan != null
|
final tanggal = item.tanggalPenitipan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(item.tanggalPenitipan!)
|
||||||
.format(item.tanggalPenitipan!)
|
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -214,8 +214,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
Widget _buildPenitipanCard(
|
Widget _buildPenitipanCard(
|
||||||
BuildContext context, dynamic penitipan, Color statusColor) {
|
BuildContext context, dynamic penitipan, Color statusColor) {
|
||||||
final formattedDate = penitipan.tanggalPenitipan != null
|
final formattedDate = penitipan.tanggalPenitipan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||||
.format(penitipan.tanggalPenitipan!)
|
|
||||||
: 'Tanggal tidak tersedia';
|
: 'Tanggal tidak tersedia';
|
||||||
|
|
||||||
IconData statusIcon;
|
IconData statusIcon;
|
||||||
@ -435,61 +434,6 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
return id != null ? 'Petugas Desa' : 'Tidak ada petugas';
|
return id != null ? 'Petugas Desa' : 'Tidak ada petugas';
|
||||||
}
|
}
|
||||||
|
|
||||||
void showFullScreenImage(String imageUrl) {
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
child: Image.network(
|
|
||||||
imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: loadingProgress.expectedTotalBytes != null
|
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.5),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
title: const Text('Detail Penitipan'),
|
title: const Text('Detail Penitipan'),
|
||||||
@ -509,8 +453,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Penitipan',
|
'Tanggal Penitipan',
|
||||||
penitipan.tanggalPenitipan != null
|
penitipan.tanggalPenitipan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||||
.format(penitipan.tanggalPenitipan!)
|
|
||||||
: 'Tanggal tidak tersedia',
|
: 'Tanggal tidak tersedia',
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
@ -520,8 +463,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
if (penitipan.tanggalVerifikasi != null)
|
if (penitipan.tanggalVerifikasi != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Verifikasi',
|
'Tanggal Verifikasi',
|
||||||
DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
FormatHelper.formatDateTime(penitipan.tanggalVerifikasi!),
|
||||||
.format(penitipan.tanggalVerifikasi!),
|
|
||||||
),
|
),
|
||||||
if (penitipan.deskripsi != null &&
|
if (penitipan.deskripsi != null &&
|
||||||
penitipan.deskripsi!.isNotEmpty)
|
penitipan.deskripsi!.isNotEmpty)
|
||||||
@ -543,8 +485,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => ShowImageDialog.showFullScreen(
|
||||||
showFullScreenImage(penitipan.fotoBantuan!.first),
|
context,
|
||||||
|
penitipan.fotoBantuan!.first,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 200,
|
height: 200,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -572,8 +516,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => ShowImageDialog.showFullScreen(
|
||||||
showFullScreenImage(penitipan.fotoBuktiSerahTerima!),
|
context,
|
||||||
|
penitipan.fotoBuktiSerahTerima!,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 200,
|
height: 200,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
@ -4,7 +4,6 @@ import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard
|
|||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/utils/date_helper.dart';
|
|
||||||
|
|
||||||
class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
||||||
const DonaturSkemaView({super.key});
|
const DonaturSkemaView({super.key});
|
||||||
@ -549,14 +548,14 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
|||||||
|
|
||||||
int days = difference.inDays;
|
int days = difference.inDays;
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
return 'Batas waktu: ${days} hari lagi';
|
return 'Batas waktu: $days hari lagi';
|
||||||
} else {
|
} else {
|
||||||
int hours = difference.inHours;
|
int hours = difference.inHours;
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return 'Batas waktu: ${hours} jam lagi';
|
return 'Batas waktu: $hours jam lagi';
|
||||||
} else {
|
} else {
|
||||||
int minutes = difference.inMinutes;
|
int minutes = difference.inMinutes;
|
||||||
return 'Batas waktu: ${minutes} menit lagi';
|
return 'Batas waktu: $minutes menit lagi';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -597,20 +596,20 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Format nilai sebagai Rupiah menggunakan DateHelper
|
// Format nilai sebagai Rupiah menggunakan DateHelper
|
||||||
return DateHelper.formatRupiah(nilai);
|
return FormatHelper.formatRupiah(nilai);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika bukan uang, kembalikan nilai + satuan (jika ada)
|
// Jika bukan uang, kembalikan nilai + satuan (jika ada)
|
||||||
return '${jumlahDiterimaPerOrang} ${stokBantuan.satuan ?? ''}';
|
return '$jumlahDiterimaPerOrang ${stokBantuan.satuan ?? ''}';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatRupiah(dynamic amount) {
|
String _formatRupiah(dynamic amount) {
|
||||||
if (amount is num) {
|
if (amount is num) {
|
||||||
return DateHelper.formatRupiah(amount);
|
return FormatHelper.formatRupiah(amount);
|
||||||
} else if (amount is String) {
|
} else if (amount is String) {
|
||||||
try {
|
try {
|
||||||
double nilai = double.parse(amount);
|
double nilai = double.parse(amount);
|
||||||
return DateHelper.formatRupiah(nilai);
|
return FormatHelper.formatRupiah(nilai);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
|||||||
title: Obx(() {
|
title: Obx(() {
|
||||||
switch (controller.activeTabIndex.value) {
|
switch (controller.activeTabIndex.value) {
|
||||||
case 0:
|
case 0:
|
||||||
return const Text('Dashboard Donatur');
|
return const Text('Dashboard');
|
||||||
case 1:
|
case 1:
|
||||||
return const Text('Skema Bantuan');
|
return const Text('Skema Bantuan');
|
||||||
case 2:
|
case 2:
|
||||||
@ -44,7 +44,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
|||||||
case 4:
|
case 4:
|
||||||
return const Text('Laporan Penyaluran');
|
return const Text('Laporan Penyaluran');
|
||||||
default:
|
default:
|
||||||
return const Text('Dashboard Donatur');
|
return const Text('Dashboard');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -201,12 +201,20 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
|||||||
controller.profilePhotoUrl!.isNotEmpty
|
controller.profilePhotoUrl!.isNotEmpty
|
||||||
? NetworkImage(controller.profilePhotoUrl!)
|
? NetworkImage(controller.profilePhotoUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: controller.profilePhotoUrl == null ||
|
child: (controller.profilePhotoUrl == null ||
|
||||||
controller.profilePhotoUrl!.isEmpty
|
controller.profilePhotoUrl!.isEmpty)
|
||||||
? const Icon(
|
? Text(
|
||||||
Icons.person,
|
controller.nama.isNotEmpty
|
||||||
color: Colors.white,
|
? controller.nama
|
||||||
size: 40,
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -284,44 +292,175 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
_buildMenuCategory('Menu Utama'),
|
||||||
leading: const Icon(Icons.person_outline),
|
Obx(() => _buildMenuItem(
|
||||||
title: const Text('Profil'),
|
icon: Icons.dashboard_outlined,
|
||||||
|
activeIcon: Icons.dashboard,
|
||||||
|
title: 'Dashboard',
|
||||||
|
isSelected: controller.activeTabIndex.value == 0,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.activeTabIndex.value = 0;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Obx(() => _buildMenuItem(
|
||||||
|
icon: Icons.description_outlined,
|
||||||
|
activeIcon: Icons.description,
|
||||||
|
title: 'Skema Bantuan',
|
||||||
|
isSelected: controller.activeTabIndex.value == 1,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.activeTabIndex.value = 1;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Obx(() => _buildMenuItem(
|
||||||
|
icon: Icons.calendar_today_outlined,
|
||||||
|
activeIcon: Icons.calendar_today,
|
||||||
|
title: 'Jadwal Penyaluran',
|
||||||
|
isSelected: controller.activeTabIndex.value == 2,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.activeTabIndex.value = 2;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Obx(() => _buildMenuItem(
|
||||||
|
icon: Icons.add_box_outlined,
|
||||||
|
activeIcon: Icons.add_box,
|
||||||
|
title: 'Penitipan Bantuan',
|
||||||
|
isSelected: controller.activeTabIndex.value == 3,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.activeTabIndex.value = 3;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Obx(() => _buildMenuItem(
|
||||||
|
icon: Icons.assignment_outlined,
|
||||||
|
activeIcon: Icons.assignment,
|
||||||
|
title: 'Laporan Penyaluran',
|
||||||
|
isSelected: controller.activeTabIndex.value == 4,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.activeTabIndex.value = 4;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
_buildMenuCategory('Pengaturan'),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
activeIcon: Icons.person,
|
||||||
|
title: 'Profil',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Get.toNamed('/profile');
|
Get.toNamed('/profile');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
_buildMenuItem(
|
||||||
leading: const Icon(Icons.history),
|
icon: Icons.info_outline,
|
||||||
title: const Text('Riwayat Donasi'),
|
activeIcon: Icons.info,
|
||||||
|
title: 'Tentang Kami',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
// TODO: Implementasi riwayat donasi
|
Get.toNamed('/about');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
_buildMenuItem(
|
||||||
leading: const Icon(Icons.settings_outlined),
|
icon: Icons.logout,
|
||||||
title: const Text('Pengaturan'),
|
title: 'Keluar',
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// TODO: Implementasi pengaturan
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.logout),
|
|
||||||
title: const Text('Keluar'),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
controller.logout();
|
controller.logout();
|
||||||
},
|
},
|
||||||
|
isLogout: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
'© ${DateTime.now().year} DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuCategory(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuItem({
|
||||||
|
required IconData icon,
|
||||||
|
IconData? activeIcon,
|
||||||
|
required String title,
|
||||||
|
bool isSelected = false,
|
||||||
|
String? badge,
|
||||||
|
required Function() onTap,
|
||||||
|
bool isLogout = false,
|
||||||
|
}) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? AppTheme.primaryColor.withOpacity(0.1)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isSelected ? (activeIcon ?? icon) : icon,
|
||||||
|
color: isSelected
|
||||||
|
? AppTheme.primaryColor
|
||||||
|
: (isLogout ? Colors.red : null),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? AppTheme.primaryColor
|
||||||
|
: (isLogout ? Colors.red : null),
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: badge != null
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 20,
|
||||||
|
minHeight: 20,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
badge,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:open_file/open_file.dart';
|
import 'package:open_file/open_file.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
|
|
||||||
@ -635,7 +634,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: PdfColors.blue900)),
|
color: PdfColors.blue900)),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
'Tanggal: ${DateFormat('dd MMMM yyyy').format(DateTime.now())}',
|
'Tanggal: ${FormatHelper.formatDateTime(DateTime.now())}',
|
||||||
style: pw.TextStyle(font: ttf, fontSize: 10),
|
style: pw.TextStyle(font: ttf, fontSize: 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -708,8 +707,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
_buildPdfRow(
|
_buildPdfRow(
|
||||||
'Tanggal Laporan',
|
'Tanggal Laporan',
|
||||||
laporan.tanggalLaporan != null
|
laporan.tanggalLaporan != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(laporan.tanggalLaporan!)
|
||||||
laporan.tanggalLaporan!)
|
|
||||||
: '-',
|
: '-',
|
||||||
ttf,
|
ttf,
|
||||||
ttfBold),
|
ttfBold),
|
||||||
@ -731,7 +729,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
_buildPdfRow(
|
_buildPdfRow(
|
||||||
'Tanggal Penyaluran',
|
'Tanggal Penyaluran',
|
||||||
penyaluran.tanggalPenyaluran != null
|
penyaluran.tanggalPenyaluran != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
penyaluran.tanggalPenyaluran!)
|
penyaluran.tanggalPenyaluran!)
|
||||||
: '-',
|
: '-',
|
||||||
ttf,
|
ttf,
|
||||||
@ -739,7 +737,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
_buildPdfRow(
|
_buildPdfRow(
|
||||||
'Tanggal Selesai',
|
'Tanggal Selesai',
|
||||||
penyaluran.tanggalSelesai != null
|
penyaluran.tanggalSelesai != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
penyaluran.tanggalSelesai!)
|
penyaluran.tanggalSelesai!)
|
||||||
: '-',
|
: '-',
|
||||||
ttf,
|
ttf,
|
||||||
@ -902,7 +900,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
|
|
||||||
final isUang = stokBantuan['is_uang'] == true;
|
final isUang = stokBantuan['is_uang'] == true;
|
||||||
final formattedJumlah = isUang
|
final formattedJumlah = isUang
|
||||||
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}'
|
? FormatHelper.formatRupiah(jumlah)
|
||||||
: '$jumlah ${stokBantuan['satuan'] ?? ''}';
|
: '$jumlah ${stokBantuan['satuan'] ?? ''}';
|
||||||
|
|
||||||
return pw.TableRow(
|
return pw.TableRow(
|
||||||
@ -975,7 +973,7 @@ class LaporanPenyaluranController extends GetxController {
|
|||||||
final jumlahBantuan = penerima.jumlahBantuan ?? 0;
|
final jumlahBantuan = penerima.jumlahBantuan ?? 0;
|
||||||
|
|
||||||
final formattedJumlah = isUang
|
final formattedJumlah = isUang
|
||||||
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlahBantuan)}'
|
? FormatHelper.formatRupiah(jumlahBantuan)
|
||||||
: '$jumlahBantuan ${penerima.satuan ?? ''}';
|
: '$jumlahBantuan ${penerima.satuan ?? ''}';
|
||||||
|
|
||||||
return pw.TableRow(
|
return pw.TableRow(
|
||||||
|
@ -65,7 +65,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
|||||||
controller.selectedPenyaluran.value!
|
controller.selectedPenyaluran.value!
|
||||||
.tanggalPenyaluran !=
|
.tanggalPenyaluran !=
|
||||||
null
|
null
|
||||||
? DateTimeHelper.formatDateTime(controller
|
? FormatHelper.formatDateTime(controller
|
||||||
.selectedPenyaluran.value!.tanggalPenyaluran!)
|
.selectedPenyaluran.value!.tanggalPenyaluran!)
|
||||||
: '-',
|
: '-',
|
||||||
),
|
),
|
||||||
@ -73,7 +73,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
|||||||
'Tanggal Selesai',
|
'Tanggal Selesai',
|
||||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||||
null
|
null
|
||||||
? DateTimeHelper.formatDateTime(controller
|
? FormatHelper.formatDateTime(controller
|
||||||
.selectedPenyaluran.value!.tanggalSelesai!)
|
.selectedPenyaluran.value!.tanggalSelesai!)
|
||||||
: '-',
|
: '-',
|
||||||
),
|
),
|
||||||
|
@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
|
|||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||||
const LaporanPenyaluranView({super.key});
|
const LaporanPenyaluranView({super.key});
|
||||||
@ -255,8 +254,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
// const SizedBox(width: 8),
|
||||||
StatusBadge(status: laporan.status ?? 'DRAFT'),
|
// StatusBadge(status: laporan.status ?? 'DRAFT'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -273,10 +272,11 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
|||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
'Tanggal',
|
'Tanggal',
|
||||||
laporan.tanggalLaporan != null
|
laporan.tanggalLaporan != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
laporan.tanggalLaporan!)
|
laporan.tanggalLaporan!)
|
||||||
: '-',
|
: '-',
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
_buildInfoItem(
|
_buildInfoItem(
|
||||||
Icons.description,
|
Icons.description,
|
||||||
'Status',
|
'Status',
|
||||||
@ -538,8 +538,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
penyaluran.tanggalSelesai != null
|
penyaluran.tanggalSelesai != null
|
||||||
? DateFormat('dd/MM/yyyy')
|
? FormatHelper.formatDateTime(
|
||||||
.format(penyaluran.tanggalSelesai!)
|
penyaluran.tanggalSelesai!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
|
||||||
|
|
||||||
|
class JadwalPenyaluranBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.lazyPut<JadwalPenyaluranController>(
|
||||||
|
() => JadwalPenyaluranController(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register service untuk komunikasi pembaruan jadwal
|
||||||
|
if (!Get.isRegistered<JadwalUpdateService>()) {
|
||||||
|
Get.lazyPut<JadwalUpdateService>(
|
||||||
|
() => JadwalUpdateService(),
|
||||||
|
fenix: true, // Pastikan service tetap aktif selama aplikasi berjalan
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -298,7 +298,7 @@ class CalendarViewWidget extends StatelessWidget {
|
|||||||
for (var jadwal in allJadwal) {
|
for (var jadwal in allJadwal) {
|
||||||
if (jadwal.tanggalPenyaluran != null) {
|
if (jadwal.tanggalPenyaluran != null) {
|
||||||
DateTime jadwalDate =
|
DateTime jadwalDate =
|
||||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||||
|
|
||||||
if (jadwalDate
|
if (jadwalDate
|
||||||
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
|
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
|
||||||
@ -346,7 +346,7 @@ class CalendarViewWidget extends StatelessWidget {
|
|||||||
|
|
||||||
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
|
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
|
||||||
final String formattedDate =
|
final String formattedDate =
|
||||||
DateTimeHelper.formatDateIndonesian(appointment.startTime);
|
FormatHelper.formatDateIndonesian(appointment.startTime);
|
||||||
|
|
||||||
// Dapatkan status dari ID jadwal
|
// Dapatkan status dari ID jadwal
|
||||||
String? status = _getStatusFromAppointmentId(appointment.id);
|
String? status = _getStatusFromAppointmentId(appointment.id);
|
||||||
|
@ -207,7 +207,7 @@ class JadwalSectionWidget extends StatelessWidget {
|
|||||||
|
|
||||||
// Format tanggal dan waktu menggunakan helper
|
// Format tanggal dan waktu menggunakan helper
|
||||||
String formattedDateTime =
|
String formattedDateTime =
|
||||||
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
|
FormatHelper.formatDateTime(jadwal.tanggalPenyaluran);
|
||||||
|
|
||||||
// Dapatkan nama lokasi dan kategori
|
// Dapatkan nama lokasi dan kategori
|
||||||
String lokasiName =
|
String lokasiName =
|
||||||
|
@ -211,18 +211,16 @@ class DetailPenyaluranController extends GetxController {
|
|||||||
.eq('id', penerima.id!)
|
.eq('id', penerima.id!)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (penerimaData != null) {
|
final String stokBantuanId = penerimaData['stok_bantuan_id'];
|
||||||
final String stokBantuanId = penerimaData['stok_bantuan_id'];
|
final double jumlah = penerimaData['jumlah_bantuan'] is int
|
||||||
final double jumlah = penerimaData['jumlah_bantuan'] is int
|
? penerimaData['jumlah_bantuan'].toDouble()
|
||||||
? penerimaData['jumlah_bantuan'].toDouble()
|
: penerimaData['jumlah_bantuan'];
|
||||||
: penerimaData['jumlah_bantuan'];
|
|
||||||
|
|
||||||
// Kurangi stok dan catat riwayat
|
// Kurangi stok dan catat riwayat
|
||||||
final petugasId = _supabaseService.client.auth.currentUser?.id;
|
final petugasId = _supabaseService.client.auth.currentUser?.id;
|
||||||
if (petugasId != null) {
|
if (petugasId != null) {
|
||||||
await _supabaseService.kurangiStokDariPenyaluran(
|
await _supabaseService.kurangiStokDariPenyaluran(
|
||||||
penerima.id!, stokBantuanId, jumlah, petugasId);
|
penerima.id!, stokBantuanId, jumlah, petugasId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data setelah konfirmasi berhasil
|
// Refresh data setelah konfirmasi berhasil
|
||||||
|
@ -11,14 +11,21 @@ import 'package:penyaluran_app/app/utils/format_helper.dart';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/notification_service.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||||
|
|
||||||
class JadwalPenyaluranController extends GetxController {
|
class JadwalPenyaluranController extends GetxController {
|
||||||
final AuthController _authController = Get.find<AuthController>();
|
final AuthController _authController = Get.find<AuthController>();
|
||||||
final SupabaseService _supabaseService = SupabaseService.to;
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
|
late final JadwalUpdateService _jadwalUpdateService;
|
||||||
|
late final StreamSubscription _jadwalUpdateSubscription;
|
||||||
|
|
||||||
SupabaseService get supabaseService => _supabaseService;
|
SupabaseService get supabaseService => _supabaseService;
|
||||||
|
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxBool isLoadingStatusUpdate = false.obs;
|
||||||
|
final RxBool isLokasiLoading = false.obs;
|
||||||
|
|
||||||
// Indeks kategori yang dipilih untuk filter
|
// Indeks kategori yang dipilih untuk filter
|
||||||
final RxInt selectedCategoryIndex = 0.obs;
|
final RxInt selectedCategoryIndex = 0.obs;
|
||||||
@ -52,6 +59,21 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
|
// Inisialisasi JadwalUpdateService
|
||||||
|
if (Get.isRegistered<JadwalUpdateService>()) {
|
||||||
|
_jadwalUpdateService = Get.find<JadwalUpdateService>();
|
||||||
|
} else {
|
||||||
|
_jadwalUpdateService = Get.put(JadwalUpdateService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daftarkan controller ini untuk menerima pembaruan
|
||||||
|
_jadwalUpdateService.registerForUpdates('JadwalPenyaluranController');
|
||||||
|
|
||||||
|
// Berlangganan ke pembaruan jadwal
|
||||||
|
_jadwalUpdateSubscription =
|
||||||
|
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
|
||||||
|
|
||||||
loadJadwalData();
|
loadJadwalData();
|
||||||
loadPermintaanPenjadwalanData();
|
loadPermintaanPenjadwalanData();
|
||||||
loadLokasiPenyaluranData();
|
loadLokasiPenyaluranData();
|
||||||
@ -67,100 +89,444 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
searchController.dispose();
|
searchController.dispose();
|
||||||
// Hentikan timer jika ada
|
// Hentikan timer jika ada
|
||||||
_stopJadwalCheckTimer();
|
_stopJadwalCheckTimer();
|
||||||
|
// Berhenti berlangganan pembaruan jadwal
|
||||||
|
_jadwalUpdateSubscription.cancel();
|
||||||
|
// Batalkan pendaftaran controller
|
||||||
|
_jadwalUpdateService.unregisterFromUpdates('JadwalPenyaluranController');
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer untuk memeriksa jadwal secara berkala
|
// Timer untuk memeriksa jadwal secara berkala
|
||||||
Timer? _jadwalCheckTimer;
|
Timer? _jadwalCheckTimer;
|
||||||
|
Timer?
|
||||||
|
_intensiveCheckTimer; // Timer untuk pengecekan intensif mendekati waktu penyaluran
|
||||||
|
final RxBool _intensiveCheckActive = false.obs; // Status pengecekan intensif
|
||||||
|
|
||||||
void _startJadwalCheckTimer() {
|
void _startJadwalCheckTimer() {
|
||||||
// Periksa jadwal setiap 1 menit
|
// Dengan fitur realtime yang sudah aktif, kita bisa mengurangi frekuensi polling
|
||||||
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
// Cek setiap 30 detik sebagai fallback untuk realtime
|
||||||
checkAndUpdateJadwalStatus();
|
_jadwalCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||||
|
if (!isLoadingStatusUpdate.value) {
|
||||||
|
checkAndUpdateJadwalStatus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Periksa jadwal segera saat aplikasi dimulai
|
// Periksa jadwal segera saat aplikasi dimulai
|
||||||
checkAndUpdateJadwalStatus();
|
checkAndUpdateJadwalStatus();
|
||||||
|
|
||||||
|
// Log info untuk debugging
|
||||||
|
print('Jadwal check timer started with 30 seconds interval');
|
||||||
|
|
||||||
|
// Mulai juga pengecekan jadwal yang akan datang
|
||||||
|
_startUpcomingJadwalCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stopJadwalCheckTimer() {
|
void _stopJadwalCheckTimer() {
|
||||||
_jadwalCheckTimer?.cancel();
|
_jadwalCheckTimer?.cancel();
|
||||||
_jadwalCheckTimer = null;
|
_jadwalCheckTimer = null;
|
||||||
|
_intensiveCheckTimer?.cancel();
|
||||||
|
_intensiveCheckTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode baru untuk memeriksa jadwal mendatang dan memulai pemeriksaan intensif jika perlu
|
||||||
|
void _startUpcomingJadwalCheck() {
|
||||||
|
Timer.periodic(const Duration(minutes: 1), (timer) {
|
||||||
|
// Jika sudah ada timer intensif yang berjalan, tidak perlu melakukan pengecekan lagi
|
||||||
|
if (_intensiveCheckActive.value) return;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
bool foundUpcomingJadwal = false;
|
||||||
|
|
||||||
|
// Periksa apakah ada jadwal yang akan aktif dalam 10 menit ke depan
|
||||||
|
for (var jadwal in jadwalMendatang) {
|
||||||
|
if (jadwal.tanggalPenyaluran != null &&
|
||||||
|
jadwal.status == 'DIJADWALKAN') {
|
||||||
|
final jadwalTime = jadwal.tanggalPenyaluran!;
|
||||||
|
final diff = jadwalTime.difference(now).inMinutes;
|
||||||
|
|
||||||
|
// Jika ada jadwal dalam 10 menit ke depan, mulai pemeriksaan intensif
|
||||||
|
if (diff >= 0 && diff <= 10) {
|
||||||
|
print(
|
||||||
|
'Found upcoming jadwal in $diff minutes: ${jadwal.id} - ${jadwal.nama}');
|
||||||
|
foundUpcomingJadwal = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika ditemukan jadwal yang akan datang, mulai pemeriksaan intensif
|
||||||
|
if (foundUpcomingJadwal && !_intensiveCheckActive.value) {
|
||||||
|
_startIntensiveCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk memulai pemeriksaan intensif untuk jadwal yang mendekati waktu
|
||||||
|
void _startIntensiveCheck() {
|
||||||
|
if (_intensiveCheckActive.value) return;
|
||||||
|
|
||||||
|
_intensiveCheckActive.value = true;
|
||||||
|
print('Starting intensive jadwal check every 5 seconds');
|
||||||
|
|
||||||
|
// Periksa setiap 5 detik
|
||||||
|
_intensiveCheckTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||||
|
if (!isLoadingStatusUpdate.value) {
|
||||||
|
checkAndUpdateJadwalStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periksa apakah masih perlu melakukan pemeriksaan intensif
|
||||||
|
final now = DateTime.now();
|
||||||
|
bool needIntensiveCheck = false;
|
||||||
|
|
||||||
|
for (var jadwal in jadwalMendatang) {
|
||||||
|
if (jadwal.tanggalPenyaluran != null &&
|
||||||
|
jadwal.status == 'DIJADWALKAN') {
|
||||||
|
final jadwalTime = jadwal.tanggalPenyaluran!;
|
||||||
|
final diff = jadwalTime.difference(now).inMinutes;
|
||||||
|
|
||||||
|
// Jika masih ada jadwal dalam 10 menit ke depan, lanjutkan pemeriksaan
|
||||||
|
if (diff >= -5 && diff <= 10) {
|
||||||
|
needIntensiveCheck = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada lagi jadwal yang mendekati waktu, hentikan pemeriksaan intensif
|
||||||
|
if (!needIntensiveCheck) {
|
||||||
|
_stopIntensiveCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk menghentikan pemeriksaan intensif
|
||||||
|
void _stopIntensiveCheck() {
|
||||||
|
_intensiveCheckTimer?.cancel();
|
||||||
|
_intensiveCheckTimer = null;
|
||||||
|
_intensiveCheckActive.value = false;
|
||||||
|
print('Stopping intensive jadwal check');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler untuk menerima pembaruan jadwal dari service
|
||||||
|
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
|
||||||
|
if (updateData['type'] == 'status_update') {
|
||||||
|
// Update lokal jika jadwal yang diperbarui ada di salah satu list
|
||||||
|
final jadwalId = updateData['jadwal_id'];
|
||||||
|
final newStatus = updateData['new_status'];
|
||||||
|
|
||||||
|
// Periksa dan update jadwal di berbagai daftar
|
||||||
|
_updateJadwalStatusLocally(jadwalId, newStatus);
|
||||||
|
} else if (updateData['type'] == 'reload_required') {
|
||||||
|
// Muat ulang data jika diminta
|
||||||
|
loadJadwalData();
|
||||||
|
loadPermintaanPenjadwalanData();
|
||||||
|
} else if (updateData['type'] == 'check_required') {
|
||||||
|
// Segera periksa status jadwal
|
||||||
|
if (!isLoadingStatusUpdate.value) {
|
||||||
|
print(
|
||||||
|
'Received check_required signal, checking jadwal status immediately');
|
||||||
|
checkAndUpdateJadwalStatus();
|
||||||
|
} else {
|
||||||
|
print('Already checking jadwal status, ignoring check_required signal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perbarui status jadwal secara lokal tanpa perlu memanggil API lagi
|
||||||
|
void _updateJadwalStatusLocally(String jadwalId, String newStatus) {
|
||||||
|
bool updated = false;
|
||||||
|
print(
|
||||||
|
'Updating jadwal status locally - ID: $jadwalId, New Status: $newStatus');
|
||||||
|
|
||||||
|
// Periksa jadwal aktif
|
||||||
|
final jadwalAktifIndex =
|
||||||
|
jadwalAktif.indexWhere((jadwal) => jadwal.id == jadwalId);
|
||||||
|
if (jadwalAktifIndex >= 0) {
|
||||||
|
print('Found in jadwalAktif at index $jadwalAktifIndex');
|
||||||
|
jadwalAktif[jadwalAktifIndex] =
|
||||||
|
jadwalAktif[jadwalAktifIndex].copyWith(status: newStatus);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periksa jadwal mendatang
|
||||||
|
final jadwalMendatangIndex =
|
||||||
|
jadwalMendatang.indexWhere((jadwal) => jadwal.id == jadwalId);
|
||||||
|
if (jadwalMendatangIndex >= 0) {
|
||||||
|
print('Found in jadwalMendatang at index $jadwalMendatangIndex');
|
||||||
|
jadwalMendatang[jadwalMendatangIndex] =
|
||||||
|
jadwalMendatang[jadwalMendatangIndex].copyWith(status: newStatus);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periksa jadwal terlaksana
|
||||||
|
final jadwalTerlaksanaIndex =
|
||||||
|
jadwalTerlaksana.indexWhere((jadwal) => jadwal.id == jadwalId);
|
||||||
|
if (jadwalTerlaksanaIndex >= 0) {
|
||||||
|
print('Found in jadwalTerlaksana at index $jadwalTerlaksanaIndex');
|
||||||
|
jadwalTerlaksana[jadwalTerlaksanaIndex] =
|
||||||
|
jadwalTerlaksana[jadwalTerlaksanaIndex].copyWith(status: newStatus);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika perlu, reorganisasi daftar berdasarkan status baru
|
||||||
|
if (updated) {
|
||||||
|
print('Status updated locally, reorganizing lists');
|
||||||
|
_reorganizeJadwalLists();
|
||||||
|
|
||||||
|
// Perbarui counter penyaluran setelah reorganisasi daftar
|
||||||
|
_updatePenyaluranCounters();
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'Jadwal with ID $jadwalId not found in any list, refreshing data from server');
|
||||||
|
// Jika jadwal tidak ditemukan di daftar lokal, muat ulang data
|
||||||
|
loadJadwalData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorganisasi daftar jadwal berdasarkan status mereka
|
||||||
|
void _reorganizeJadwalLists() {
|
||||||
|
// Filter jadwal yang seharusnya pindah dari satu list ke list lain
|
||||||
|
|
||||||
|
// Jadwal yang seharusnya pindah dari aktif ke terlaksana
|
||||||
|
final completedJadwal = jadwalAktif
|
||||||
|
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
|
||||||
|
.toList();
|
||||||
|
if (completedJadwal.isNotEmpty) {
|
||||||
|
jadwalAktif.removeWhere(
|
||||||
|
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
|
||||||
|
jadwalTerlaksana.addAll(completedJadwal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jadwal yang seharusnya pindah dari mendatang ke aktif
|
||||||
|
final activeJadwal =
|
||||||
|
jadwalMendatang.where((j) => j.status == 'AKTIF').toList();
|
||||||
|
if (activeJadwal.isNotEmpty) {
|
||||||
|
jadwalMendatang.removeWhere((j) => j.status == 'AKTIF');
|
||||||
|
jadwalAktif.addAll(activeJadwal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jadwal yang seharusnya pindah dari mendatang ke terlaksana
|
||||||
|
final expiredJadwal = jadwalMendatang
|
||||||
|
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
|
||||||
|
.toList();
|
||||||
|
if (expiredJadwal.isNotEmpty) {
|
||||||
|
jadwalMendatang.removeWhere(
|
||||||
|
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
|
||||||
|
jadwalTerlaksana.addAll(expiredJadwal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memicu pembaruan UI
|
||||||
|
jadwalAktif.refresh();
|
||||||
|
jadwalMendatang.refresh();
|
||||||
|
jadwalTerlaksana.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode baru untuk memperbarui counter penyaluran
|
||||||
|
void _updatePenyaluranCounters() {
|
||||||
|
try {
|
||||||
|
// Dapatkan jumlah jadwal untuk setiap status
|
||||||
|
int dijadwalkan =
|
||||||
|
jadwalMendatang.where((j) => j.status == 'DIJADWALKAN').length;
|
||||||
|
int aktif = jadwalAktif.where((j) => j.status == 'AKTIF').length;
|
||||||
|
int batal =
|
||||||
|
jadwalTerlaksana.where((j) => j.status == 'BATALTERLAKSANA').length;
|
||||||
|
int terlaksana =
|
||||||
|
jadwalTerlaksana.where((j) => j.status == 'TERLAKSANA').length;
|
||||||
|
|
||||||
|
// Hitung total jadwal aktif untuk tab hari ini
|
||||||
|
int jadwalHariIni = jadwalAktif.length;
|
||||||
|
|
||||||
|
// Perbarui counter jadwal
|
||||||
|
if (Get.isRegistered<CounterService>()) {
|
||||||
|
final counterService = Get.find<CounterService>();
|
||||||
|
counterService.updateJadwalCounter(jadwalHariIni);
|
||||||
|
}
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Jadwal counters updated - Aktif: $aktif, Dijadwalkan: $dijadwalkan, Terlaksana: $terlaksana, Batal: $batal');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating jadwal counters: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memeriksa dan memperbarui status jadwal
|
// Memeriksa dan memperbarui status jadwal
|
||||||
Future<void> checkAndUpdateJadwalStatus() async {
|
Future<void> checkAndUpdateJadwalStatus() async {
|
||||||
|
if (isLoadingStatusUpdate.value) return;
|
||||||
|
|
||||||
|
isLoadingStatusUpdate.value = true;
|
||||||
|
print('Starting jadwal status check at ${DateTime.now()}');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
List<PenyaluranBantuanModel> jadwalToUpdate = [];
|
// Kelompokkan jadwal yang perlu diperbarui untuk mengurangi jumlah operasi database
|
||||||
List<PenyaluranBantuanModel> jadwalTerlewat = [];
|
final Map<String, String> jadwalUpdates = {};
|
||||||
|
final List<PenyaluranBantuanModel> jadwalToUpdate = [];
|
||||||
|
final List<PenyaluranBantuanModel> jadwalTerlewat = [];
|
||||||
|
|
||||||
for (var jadwal in jadwalAktif) {
|
print('Checking ${jadwalMendatang.length} upcoming schedules');
|
||||||
if (jadwal.tanggalPenyaluran != null) {
|
|
||||||
final jadwalDateTime =
|
|
||||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
|
||||||
final jadwalDate = DateTime(
|
|
||||||
jadwalDateTime.year,
|
|
||||||
jadwalDateTime.month,
|
|
||||||
jadwalDateTime.day,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSameDay(jadwalDate, today)) {
|
// Proses semua jadwal yang perlu diperbarui
|
||||||
if (now.isAfter(jadwalDateTime) ||
|
for (var jadwal in jadwalMendatang) {
|
||||||
now.isAtSameMomentAs(jadwalDateTime)) {
|
if (jadwal.tanggalPenyaluran != null && jadwal.id != null) {
|
||||||
if (jadwal.status == 'DIJADWALKAN') {
|
final jadwalDate = jadwal.tanggalPenyaluran!;
|
||||||
if (now
|
|
||||||
.isBefore(jadwalDateTime.add(const Duration(hours: 2)))) {
|
// Log untuk debugging waktu pemeriksaan
|
||||||
await _supabaseService.updateJadwalStatus(
|
print(
|
||||||
jadwal.id!, 'AKTIF');
|
'Checking jadwal: ${jadwal.id} - ${jadwal.nama} scheduled for ${jadwal.tanggalPenyaluran}');
|
||||||
jadwalToUpdate.add(jadwal);
|
print('Current time: $now, Jadwal time: $jadwalDate');
|
||||||
} else {
|
|
||||||
await _supabaseService.updateJadwalStatus(
|
// Periksa apakah jadwal sudah melewati waktunya
|
||||||
jadwal.id!, 'BATALTERLAKSANA');
|
// Kita gunakan isAtSameMomentAs atau isAfter untuk menangkap dengan tepat
|
||||||
jadwalTerlewat.add(jadwal);
|
if (now.isAfter(jadwalDate) || now.isAtSameMomentAs(jadwalDate)) {
|
||||||
}
|
print('Jadwal time has passed/reached for ${jadwal.id}');
|
||||||
} else if (jadwal.status == 'AKTIF') {
|
|
||||||
if (now.isAfter(jadwalDateTime.add(const Duration(hours: 2)))) {
|
// Batasan 2 jam untuk status aktif
|
||||||
await _supabaseService.updateJadwalStatus(
|
final batasAktif = jadwalDate.add(const Duration(hours: 2));
|
||||||
jadwal.id!, 'BATALTERLAKSANA');
|
|
||||||
jadwalTerlewat.add(jadwal);
|
if (jadwal.status == 'DIJADWALKAN' && now.isBefore(batasAktif)) {
|
||||||
}
|
print(
|
||||||
|
'Updating to AKTIF: ${jadwal.id} - Time difference: ${now.difference(jadwalDate).inSeconds} seconds');
|
||||||
|
jadwalUpdates[jadwal.id!] = 'AKTIF';
|
||||||
|
jadwalToUpdate.add(jadwal);
|
||||||
|
} else if ((jadwal.status == 'DIJADWALKAN' ||
|
||||||
|
jadwal.status == 'AKTIF') &&
|
||||||
|
now.isAfter(batasAktif)) {
|
||||||
|
print('Updating to BATALTERLAKSANA (time expired): ${jadwal.id}');
|
||||||
|
jadwalUpdates[jadwal.id!] = 'BATALTERLAKSANA';
|
||||||
|
jadwalTerlewat.add(jadwal);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Periksa apakah jadwal hampir memasuki waktunya (dalam 5 menit ke depan)
|
||||||
|
final diff = jadwalDate.difference(now).inMinutes;
|
||||||
|
if (diff >= 0 && diff <= 5 && jadwal.status == 'DIJADWALKAN') {
|
||||||
|
print('Jadwal will be active in $diff minutes: ${jadwal.id}');
|
||||||
|
|
||||||
|
// Tambahkan jadwal ke daftar pengawasan intensif
|
||||||
|
_jadwalUpdateService.addJadwalToWatch(jadwal.id!, jadwalDate);
|
||||||
|
|
||||||
|
// Jika tinggal 1 menit atau kurang, cek setiap 15 detik
|
||||||
|
if (diff <= 1) {
|
||||||
|
Future.delayed(const Duration(seconds: 15), () {
|
||||||
|
if (!isLoadingStatusUpdate.value) {
|
||||||
|
checkAndUpdateJadwalStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jadwalToUpdate.isNotEmpty || jadwalTerlewat.isNotEmpty) {
|
// Update database hanya jika ada perubahan
|
||||||
await loadJadwalData();
|
if (jadwalUpdates.isNotEmpty) {
|
||||||
|
print('Batch updating ${jadwalUpdates.length} schedules');
|
||||||
|
|
||||||
if (jadwalToUpdate.isNotEmpty) {
|
try {
|
||||||
Get.snackbar(
|
// Gunakan batch update untuk meningkatkan efisiensi
|
||||||
'Jadwal Diperbarui',
|
await _supabaseService.batchUpdateJadwalStatus(jadwalUpdates);
|
||||||
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
|
|
||||||
snackPosition: SnackPosition.TOP,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
duration: const Duration(seconds: 3),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jadwalTerlewat.isNotEmpty) {
|
// Perbarui data lokal
|
||||||
Get.snackbar(
|
await loadJadwalData();
|
||||||
'Jadwal Terlewat',
|
|
||||||
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
|
// Beritahu seluruh aplikasi tentang pembaruan
|
||||||
snackPosition: SnackPosition.TOP,
|
await _jadwalUpdateService.notifyJadwalUpdate();
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
colorText: Colors.white,
|
// Kirim notifikasi untuk perubahan status jadwal
|
||||||
duration: const Duration(seconds: 3),
|
bool notificationsSuccessful = true;
|
||||||
);
|
final notificationService = Get.find<NotificationService>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Kirim notifikasi untuk jadwal yang diperbarui menjadi Aktif
|
||||||
|
for (var jadwal in jadwalToUpdate) {
|
||||||
|
if (jadwal.id != null && jadwal.nama != null) {
|
||||||
|
await notificationService.sendJadwalStatusNotification(
|
||||||
|
jadwalId: jadwal.id!,
|
||||||
|
newStatus: 'AKTIF',
|
||||||
|
jadwalNama: jadwal.nama!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (notificationError) {
|
||||||
|
print(
|
||||||
|
'Warning: Error sending AKTIF notifications: $notificationError');
|
||||||
|
notificationsSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Kirim notifikasi untuk jadwal yang terlewat
|
||||||
|
for (var jadwal in jadwalTerlewat) {
|
||||||
|
if (jadwal.id != null && jadwal.nama != null) {
|
||||||
|
await notificationService.sendJadwalStatusNotification(
|
||||||
|
jadwalId: jadwal.id!,
|
||||||
|
newStatus: 'BATALTERLAKSANA',
|
||||||
|
jadwalNama: jadwal.nama!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (notificationError) {
|
||||||
|
print(
|
||||||
|
'Warning: Error sending BATALTERLAKSANA notifications: $notificationError');
|
||||||
|
notificationsSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan notifikasi hanya jika ada perubahan
|
||||||
|
if (jadwalToUpdate.isNotEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Jadwal Diperbarui',
|
||||||
|
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jadwalTerlewat.isNotEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Jadwal Terlewat',
|
||||||
|
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log status keseluruhan
|
||||||
|
if (notificationsSuccessful) {
|
||||||
|
print(
|
||||||
|
'Jadwal status update and notifications completed successfully');
|
||||||
|
} else {
|
||||||
|
print('Jadwal status update completed with notification errors');
|
||||||
|
}
|
||||||
|
} catch (updateError) {
|
||||||
|
print('Error during batch update process: $updateError');
|
||||||
|
// Jika batch update gagal, coba update satu-per-satu secara manual
|
||||||
|
print('Trying individual updates for critical jadwal...');
|
||||||
|
|
||||||
|
// Prioritaskan jadwal yang akan diaktifkan
|
||||||
|
for (var jadwal in jadwalToUpdate) {
|
||||||
|
if (jadwal.id != null) {
|
||||||
|
try {
|
||||||
|
await _supabaseService.updateJadwalStatus(jadwal.id!, 'AKTIF');
|
||||||
|
print('Manual update successful for jadwal ${jadwal.id}');
|
||||||
|
} catch (e) {
|
||||||
|
print('Manual update failed for jadwal ${jadwal.id}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print('No schedule updates needed');
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
print('Error checking and updating jadwal status: $e');
|
print('Error checking and updating jadwal status: $e');
|
||||||
print('Stack trace: $stackTrace');
|
print('Stack trace: $stackTrace');
|
||||||
|
} finally {
|
||||||
|
isLoadingStatusUpdate.value = false;
|
||||||
|
print('Jadwal status check completed at ${DateTime.now()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +563,9 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
.map((data) => PenyaluranBantuanModel.fromJson(data))
|
.map((data) => PenyaluranBantuanModel.fromJson(data))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perbarui counter penyaluran setelah data dimuat
|
||||||
|
_updatePenyaluranCounters();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading jadwal data: $e');
|
print('Error loading jadwal data: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@ -220,6 +589,7 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
|
|
||||||
Future<void> loadLokasiPenyaluranData() async {
|
Future<void> loadLokasiPenyaluranData() async {
|
||||||
try {
|
try {
|
||||||
|
isLokasiLoading(true);
|
||||||
final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
|
final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
|
||||||
if (lokasiData != null) {
|
if (lokasiData != null) {
|
||||||
for (var lokasi in lokasiData) {
|
for (var lokasi in lokasiData) {
|
||||||
@ -229,6 +599,8 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading lokasi penyaluran data: $e');
|
print('Error loading lokasi penyaluran data: $e');
|
||||||
|
} finally {
|
||||||
|
isLokasiLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,8 +707,30 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
Future<void> completeJadwal(String jadwalId) async {
|
Future<void> completeJadwal(String jadwalId) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
|
// Dapatkan detail jadwal
|
||||||
|
final jadwalIndex = jadwalAktif.indexWhere((j) => j.id == jadwalId);
|
||||||
|
PenyaluranBantuanModel? jadwal;
|
||||||
|
|
||||||
|
if (jadwalIndex >= 0) {
|
||||||
|
jadwal = jadwalAktif[jadwalIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status di database
|
||||||
await _supabaseService.completeJadwal(jadwalId);
|
await _supabaseService.completeJadwal(jadwalId);
|
||||||
|
|
||||||
|
// Kirim notifikasi
|
||||||
|
if (jadwal != null && jadwal.nama != null) {
|
||||||
|
final notificationService = Get.find<NotificationService>();
|
||||||
|
await notificationService.sendJadwalStatusNotification(
|
||||||
|
jadwalId: jadwalId,
|
||||||
|
newStatus: 'TERLAKSANA',
|
||||||
|
jadwalNama: jadwal.nama!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload data
|
||||||
await loadJadwalData();
|
await loadJadwalData();
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
'Jadwal berhasil diselesaikan',
|
'Jadwal berhasil diselesaikan',
|
||||||
@ -359,15 +753,13 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshData() async {
|
Future<void> refreshData() async {
|
||||||
isLoading.value = true;
|
await Future.wait([
|
||||||
try {
|
loadJadwalData(),
|
||||||
await loadJadwalData();
|
loadPermintaanPenjadwalanData(),
|
||||||
await loadPermintaanPenjadwalanData();
|
loadLokasiPenyaluranData(),
|
||||||
} catch (e) {
|
loadKategoriBantuanData(),
|
||||||
print('Error refreshing data: $e');
|
loadSkemaBantuanData(),
|
||||||
} finally {
|
]);
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeCategory(int index) {
|
void changeCategory(int index) {
|
||||||
@ -431,6 +823,7 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
'status_penerimaan': 'BELUMMENERIMA',
|
'status_penerimaan': 'BELUMMENERIMA',
|
||||||
'qr_code_hash': qrCodeHash,
|
'qr_code_hash': qrCodeHash,
|
||||||
'jumlah_bantuan': jumlahDiterimaPerOrang,
|
'jumlah_bantuan': jumlahDiterimaPerOrang,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simpan data penerima ke database
|
// Simpan data penerima ke database
|
||||||
|
@ -96,10 +96,10 @@ class PelaksanaanPenyaluranController extends GetxController {
|
|||||||
? response['kategori_bantuan']['nama']
|
? response['kategori_bantuan']['nama']
|
||||||
: 'Tidak tersedia',
|
: 'Tidak tersedia',
|
||||||
'tanggal': penyaluranModel.tanggalPenyaluran != null
|
'tanggal': penyaluranModel.tanggalPenyaluran != null
|
||||||
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!)
|
? FormatHelper.formatDateTime(penyaluranModel.tanggalPenyaluran!)
|
||||||
: 'Tidak tersedia',
|
: 'Tidak tersedia',
|
||||||
'waktu': penyaluranModel.tanggalPenyaluran != null
|
'waktu': penyaluranModel.tanggalPenyaluran != null
|
||||||
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
|
? FormatHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
|
||||||
: 'Tidak tersedia',
|
: 'Tidak tersedia',
|
||||||
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
|
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
|
||||||
'status': penyaluranModel.status,
|
'status': penyaluranModel.status,
|
||||||
|
@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
tanggalPenyaluran.value = DateTimeHelper.formatDate(picked);
|
tanggalPenyaluran.value = FormatHelper.formatDateTime(picked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_serv
|
|||||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
|
||||||
|
|
||||||
class PetugasDesaController extends GetxController {
|
class PetugasDesaController extends GetxController {
|
||||||
final AuthController _authController = Get.find<AuthController>();
|
final AuthController _authController = Get.find<AuthController>();
|
||||||
@ -182,10 +183,22 @@ class PetugasDesaController extends GetxController {
|
|||||||
}
|
}
|
||||||
_counterService = Get.find<CounterService>();
|
_counterService = Get.find<CounterService>();
|
||||||
|
|
||||||
|
// Pastikan JadwalUpdateService juga tersedia
|
||||||
|
JadwalUpdateService jadwalUpdateService;
|
||||||
|
if (Get.isRegistered<JadwalUpdateService>()) {
|
||||||
|
jadwalUpdateService = Get.find<JadwalUpdateService>();
|
||||||
|
} else {
|
||||||
|
jadwalUpdateService = Get.put(JadwalUpdateService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perbarui counter pada saat aplikasi dimulai
|
||||||
|
jadwalUpdateService.refreshCounters();
|
||||||
|
|
||||||
|
// Muat data awal
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
loadNotifikasiData();
|
|
||||||
loadJadwalData();
|
|
||||||
loadPenitipanData();
|
loadPenitipanData();
|
||||||
|
loadJadwalData();
|
||||||
|
loadNotifikasiData();
|
||||||
loadPengaduanData();
|
loadPengaduanData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,15 @@ import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
|
|||||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class PetugasDesaDashboardController extends GetxController {
|
class PetugasDesaDashboardController extends GetxController {
|
||||||
final AuthController _authController = Get.find<AuthController>();
|
final AuthController _authController = Get.find<AuthController>();
|
||||||
final SupabaseService _supabaseService = SupabaseService.to;
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
late final CounterService _counterService;
|
late final CounterService _counterService;
|
||||||
|
late final JadwalUpdateService _jadwalUpdateService;
|
||||||
|
late StreamSubscription _jadwalUpdateSubscription;
|
||||||
|
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
@ -67,18 +71,47 @@ class PetugasDesaDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
_counterService = Get.find<CounterService>();
|
_counterService = Get.find<CounterService>();
|
||||||
|
|
||||||
|
// Inisialisasi JadwalUpdateService untuk pembaruan realtime
|
||||||
|
if (Get.isRegistered<JadwalUpdateService>()) {
|
||||||
|
_jadwalUpdateService = Get.find<JadwalUpdateService>();
|
||||||
|
} else {
|
||||||
|
_jadwalUpdateService = Get.put(JadwalUpdateService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daftarkan controller ini untuk menerima pembaruan
|
||||||
|
_jadwalUpdateService.registerForUpdates('PetugasDesaDashboardController');
|
||||||
|
|
||||||
|
// Berlangganan ke pembaruan jadwal
|
||||||
|
_jadwalUpdateSubscription =
|
||||||
|
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
|
||||||
|
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
loadNotifikasiData();
|
loadNotifikasiData();
|
||||||
loadJadwalAktif();
|
loadJadwalHariIni();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
// Berhenti berlangganan pembaruan jadwal
|
||||||
|
_jadwalUpdateSubscription.cancel();
|
||||||
|
// Batalkan pendaftaran controller
|
||||||
|
_jadwalUpdateService
|
||||||
|
.unregisterFromUpdates('PetugasDesaDashboardController');
|
||||||
searchController.dispose();
|
searchController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler untuk menerima pembaruan jadwal dari service
|
||||||
|
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
|
||||||
|
if (updateData['type'] == 'status_update' ||
|
||||||
|
updateData['type'] == 'reload_required' ||
|
||||||
|
updateData['type'] == 'check_required') {
|
||||||
|
// Muat ulang data dashboard saat ada perubahan status jadwal
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Metode untuk memuat data profil pengguna dari cache
|
// Metode untuk memuat data profil pengguna dari cache
|
||||||
Future<void> loadUserProfile() async {
|
Future<void> loadUserProfile() async {
|
||||||
try {
|
try {
|
||||||
@ -155,14 +188,14 @@ class PetugasDesaDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadJadwalAktif() async {
|
Future<void> loadJadwalHariIni() async {
|
||||||
try {
|
try {
|
||||||
final jadwalData = await _supabaseService.getJadwalAktif();
|
final jadwalData = await _supabaseService.getJadwalAktif();
|
||||||
if (jadwalData != null) {
|
if (jadwalData != null) {
|
||||||
jadwalHariIni.value = jadwalData;
|
jadwalHariIni.value = jadwalData;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading jadwal hari ini: $e');
|
print('Error loading jadwal data: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +206,7 @@ class PetugasDesaDashboardController extends GetxController {
|
|||||||
loadUserProfile(),
|
loadUserProfile(),
|
||||||
loadDashboardData(),
|
loadDashboardData(),
|
||||||
loadNotifikasiData(),
|
loadNotifikasiData(),
|
||||||
loadJadwalAktif(),
|
loadJadwalHariIni(),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error refreshing data: $e');
|
print('Error refreshing data: $e');
|
||||||
|
@ -221,15 +221,25 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
|
|||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||||
backgroundImage: penerima['foto_profil'] != null
|
backgroundImage: penerima['foto_profil'] != null &&
|
||||||
|
penerima['foto_profil'].toString().isNotEmpty
|
||||||
? NetworkImage(penerima['foto_profil'])
|
? NetworkImage(penerima['foto_profil'])
|
||||||
: null,
|
: null,
|
||||||
child: penerima['foto_profil'] == null
|
child: (penerima['foto_profil'] == null ||
|
||||||
? Icon(
|
penerima['foto_profil'].toString().isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
size: 35,
|
penerima['nama_lengkap'] != null
|
||||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
? penerima['nama_lengkap']
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -435,13 +445,24 @@ class PenerimaSearchDelegate extends SearchDelegate {
|
|||||||
},
|
},
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
backgroundImage: penerima['foto_profil'] != null
|
backgroundImage: penerima['foto_profil'] != null &&
|
||||||
|
penerima['foto_profil'].toString().isNotEmpty
|
||||||
? NetworkImage(penerima['foto_profil'])
|
? NetworkImage(penerima['foto_profil'])
|
||||||
: null,
|
: null,
|
||||||
child: penerima['foto_profil'] == null
|
child: (penerima['foto_profil'] == null ||
|
||||||
? const Icon(
|
penerima['foto_profil'].toString().isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
color: AppTheme.primaryColor,
|
penerima['nama_lengkap'] != null
|
||||||
|
? penerima['nama_lengkap']
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
@ -33,6 +33,58 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Header DisalurKita dengan logo dan slogan
|
||||||
|
FadeInAnimation(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
const Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Header dengan greeting
|
// Header dengan greeting
|
||||||
FadeInAnimation(
|
FadeInAnimation(
|
||||||
child: GreetingHeader(
|
child: GreetingHeader(
|
||||||
@ -83,7 +135,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Jadwal Penyaluran',
|
'Jadwal Penyaluran Hari Ini',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -130,19 +182,25 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
final DateTime tanggal =
|
final DateTime tanggal =
|
||||||
DateTime.parse(jadwal['tanggal_penyaluran']);
|
DateTime.parse(jadwal['tanggal_penyaluran']);
|
||||||
final String formattedDate =
|
final String formattedDate =
|
||||||
DateTimeHelper.formatDateTime(tanggal);
|
FormatHelper.formatDateTime(tanggal);
|
||||||
final kategoriBantuan =
|
final kategoriBantuan =
|
||||||
jadwal['kategori_bantuan'] as Map<String, dynamic>;
|
jadwal['kategori_bantuan'] as Map<String, dynamic>;
|
||||||
final lokasiPenyaluran =
|
final lokasiPenyaluran =
|
||||||
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
|
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
|
||||||
|
|
||||||
return ScheduleCard(
|
return Column(
|
||||||
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
|
children: [
|
||||||
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
|
if (index > 0) const SizedBox(height: 10),
|
||||||
dateTime: formattedDate,
|
ScheduleCard(
|
||||||
isToday: true,
|
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
|
||||||
onTap: () => Get.toNamed(Routes.detailPenyaluran,
|
location:
|
||||||
parameters: {'id': jadwal['id']}),
|
lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
|
||||||
|
dateTime: formattedDate,
|
||||||
|
isToday: true,
|
||||||
|
onTap: () => Get.toNamed(Routes.detailPenyaluran,
|
||||||
|
parameters: {'id': jadwal['id']}),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -391,8 +449,10 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
|
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
|
||||||
final status = penerima['status'] ?? 'AKTIF';
|
final status = penerima['status'] ?? 'AKTIF';
|
||||||
final id = penerima['id'] ?? 'ID tidak tersedia';
|
final id = penerima['id'] ?? 'ID tidak tersedia';
|
||||||
|
final fotoProfil = penerima['foto_profil'] ?? null;
|
||||||
|
|
||||||
return _buildRecipientItem(name, nik, status, id, textTheme);
|
return _buildRecipientItem(
|
||||||
|
name, nik, status, id, textTheme, fotoProfil);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -401,8 +461,8 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRecipientItem(
|
Widget _buildRecipientItem(String name, String nik, String status, String id,
|
||||||
String name, String nik, String status, String id, TextTheme textTheme) {
|
TextTheme textTheme, String? fotoProfil) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
@ -428,7 +488,20 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundColor: Colors.white.withOpacity(0.2),
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
child: const Icon(Icons.person, color: Colors.white),
|
backgroundImage:
|
||||||
|
fotoProfil != null && fotoProfil.toString().isNotEmpty
|
||||||
|
? NetworkImage(fotoProfil)
|
||||||
|
: null,
|
||||||
|
child: (fotoProfil == null || fotoProfil.toString().isEmpty)
|
||||||
|
? Text(
|
||||||
|
name.toString().substring(0, 1).toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -5,6 +5,7 @@ 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/penitipan_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class DetailDonaturView extends GetView<DonaturController> {
|
class DetailDonaturView extends GetView<DonaturController> {
|
||||||
const DetailDonaturView({super.key});
|
const DetailDonaturView({super.key});
|
||||||
@ -359,7 +360,7 @@ class DetailDonaturView extends GetView<DonaturController> {
|
|||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
'Terdaftar Sejak',
|
'Terdaftar Sejak',
|
||||||
donatur.createdAt != null
|
donatur.createdAt != null
|
||||||
? DateTimeHelper.formatDate(donatur.createdAt!)
|
? FormatHelper.formatDateTime(donatur.createdAt!)
|
||||||
: 'Tidak diketahui',
|
: 'Tidak diketahui',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -514,7 +515,8 @@ class DetailDonaturView extends GetView<DonaturController> {
|
|||||||
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
|
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
|
||||||
final isUang = penitipan.isUang == true;
|
final isUang = penitipan.isUang == true;
|
||||||
final tanggal = penitipan.createdAt != null
|
final tanggal = penitipan.createdAt != null
|
||||||
? DateTimeHelper.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy')
|
? FormatHelper.formatDateTime(penitipan.createdAt!,
|
||||||
|
format: 'dd MMM yyyy')
|
||||||
: 'Tanggal tidak diketahui';
|
: 'Tanggal tidak diketahui';
|
||||||
|
|
||||||
String nilaiDonasi = '';
|
String nilaiDonasi = '';
|
||||||
@ -626,7 +628,7 @@ class DetailDonaturView extends GetView<DonaturController> {
|
|||||||
getPetugasDesaNama: (String? id) =>
|
getPetugasDesaNama: (String? id) =>
|
||||||
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
|
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
|
||||||
showFullScreenImage: (String imageUrl) {
|
showFullScreenImage: (String imageUrl) {
|
||||||
DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl);
|
ShowImageDialog.showFullScreen(Get.context!, imageUrl);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -107,14 +107,24 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
backgroundImage: penerima['foto_profil'] != null
|
backgroundImage: penerima['foto_profil'] != null &&
|
||||||
|
penerima['foto_profil'].toString().isNotEmpty
|
||||||
? NetworkImage(penerima['foto_profil'])
|
? NetworkImage(penerima['foto_profil'])
|
||||||
: null,
|
: null,
|
||||||
child: penerima['foto_profil'] == null
|
child: (penerima['foto_profil'] == null ||
|
||||||
? Icon(
|
penerima['foto_profil'].toString().isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
size: 60,
|
penerima['nama_lengkap'] != null
|
||||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
? penerima['nama_lengkap']
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||||
|
fontSize: 36,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -507,7 +517,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
|||||||
child: _buildInfoItem(
|
child: _buildInfoItem(
|
||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
'Tanggal Penerimaan',
|
'Tanggal Penerimaan',
|
||||||
DateTimeHelper.formatDateTime(tanggalPenerimaan),
|
FormatHelper.formatDateTime(tanggalPenerimaan),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
|
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
|
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
|
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
|
||||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
import 'package:timeline_tile/timeline_tile.dart';
|
import 'package:timeline_tile/timeline_tile.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -15,7 +14,7 @@ import 'dart:io';
|
|||||||
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
|
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
|
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class DetailPengaduanView extends GetView<PengaduanController> {
|
class DetailPengaduanView extends GetView<PengaduanController> {
|
||||||
const DetailPengaduanView({super.key});
|
const DetailPengaduanView({super.key});
|
||||||
@ -1092,8 +1091,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: tindakan.buktiTindakan!.map((bukti) {
|
children: tindakan.buktiTindakan!.map((bukti) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => ShowImageDialog.showFullScreen(
|
||||||
showFullScreenImage(context, bukti),
|
context, bukti),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -1190,8 +1189,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tindakan.tanggalTindakan != null
|
tindakan.tanggalTindakan != null
|
||||||
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
|
? FormatHelper.formatDateTime(
|
||||||
.format(tindakan.tanggalTindakan!)
|
tindakan.tanggalTindakan!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -1669,9 +1668,11 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => showFullScreenImage(
|
onTap: () => ShowImageDialog
|
||||||
stateContext,
|
.showFullScreen(
|
||||||
buktiTindakanPaths[index]),
|
stateContext,
|
||||||
|
buktiTindakanPaths[
|
||||||
|
index]),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -2003,63 +2004,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showFullScreenImage(BuildContext context, String imagePath) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
boundaryMargin: const EdgeInsets.all(20),
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4.0,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: imagePath,
|
|
||||||
placeholder: (context, url) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error, color: Colors.white, size: 32),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Gagal memuat gambar',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white, size: 30),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget untuk menampilkan feedback dan rating warga
|
|
||||||
Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) {
|
Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
@ -2348,8 +2292,7 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
pengaduan.tanggalPengaduan != null
|
pengaduan.tanggalPengaduan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||||
.format(pengaduan.tanggalPengaduan!)
|
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
@ -2376,7 +2319,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showFullScreenImage(context, url),
|
onTap: () =>
|
||||||
|
ShowImageDialog.showFullScreen(context, url),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 120,
|
width: 120,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -2589,57 +2533,4 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showFullScreenImage(BuildContext context, String imagePath) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
color: Colors.black.withOpacity(0.7),
|
|
||||||
child: Center(
|
|
||||||
child: imagePath.startsWith('http')
|
|
||||||
? CachedNetworkImage(
|
|
||||||
imageUrl: imagePath,
|
|
||||||
placeholder: (context, url) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => const Icon(
|
|
||||||
Icons.error,
|
|
||||||
color: Colors.red,
|
|
||||||
size: 50,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Image.file(File(imagePath)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 30,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -267,7 +267,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
Icons.event,
|
Icons.event,
|
||||||
'Tanggal Penyaluran',
|
'Tanggal Penyaluran',
|
||||||
penyaluran.tanggalPenyaluran != null
|
penyaluran.tanggalPenyaluran != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
penyaluran.tanggalPenyaluran!)
|
penyaluran.tanggalPenyaluran!)
|
||||||
: 'Belum dijadwalkan',
|
: 'Belum dijadwalkan',
|
||||||
AppTheme.secondaryColor),
|
AppTheme.secondaryColor),
|
||||||
@ -280,7 +280,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
Icons.event_available,
|
Icons.event_available,
|
||||||
'Tanggal Selesai',
|
'Tanggal Selesai',
|
||||||
penyaluran.tanggalSelesai != null
|
penyaluran.tanggalSelesai != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
penyaluran.tanggalSelesai!)
|
penyaluran.tanggalSelesai!)
|
||||||
: '-',
|
: '-',
|
||||||
AppTheme.secondaryColor),
|
AppTheme.secondaryColor),
|
||||||
@ -1065,19 +1065,30 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
backgroundColor: sudahMenerima
|
backgroundColor: sudahMenerima
|
||||||
? statusColor.withOpacity(0.15)
|
? statusColor.withOpacity(0.15)
|
||||||
: Colors.grey.shade50,
|
: Colors.grey.shade50,
|
||||||
child: Text(
|
backgroundImage: warga != null &&
|
||||||
warga != null && warga['nama_lengkap'] != null
|
warga['foto_profil'] != null &&
|
||||||
? warga['nama_lengkap']
|
warga['foto_profil'].toString().isNotEmpty
|
||||||
.toString()
|
? NetworkImage(warga['foto_profil'])
|
||||||
.substring(0, 1)
|
: null,
|
||||||
.toUpperCase()
|
child: (warga == null ||
|
||||||
: '?',
|
warga['foto_profil'] == null ||
|
||||||
style: TextStyle(
|
warga['foto_profil'].toString().isEmpty)
|
||||||
fontWeight: FontWeight.bold,
|
? Text(
|
||||||
color: sudahMenerima ? statusColor : Colors.grey.shade700,
|
warga != null && warga['nama_lengkap'] != null
|
||||||
fontSize: 22,
|
? warga['nama_lengkap']
|
||||||
),
|
.toString()
|
||||||
),
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: sudahMenerima
|
||||||
|
? statusColor
|
||||||
|
: Colors.grey.shade700,
|
||||||
|
fontSize: 22,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@ -1621,19 +1632,28 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 30,
|
radius: 30,
|
||||||
backgroundColor: statusColor.withOpacity(0.2),
|
backgroundColor: statusColor.withOpacity(0.2),
|
||||||
child: Text(
|
backgroundImage: warga != null &&
|
||||||
warga != null && warga['nama_lengkap'] != null
|
warga['foto_profil'] != null &&
|
||||||
? warga['nama_lengkap']
|
warga['foto_profil'].toString().isNotEmpty
|
||||||
.toString()
|
? NetworkImage(warga['foto_profil'])
|
||||||
.substring(0, 1)
|
: null,
|
||||||
.toUpperCase()
|
child: (warga == null ||
|
||||||
: '?',
|
warga['foto_profil'] == null ||
|
||||||
style: TextStyle(
|
warga['foto_profil'].toString().isEmpty)
|
||||||
fontWeight: FontWeight.bold,
|
? Text(
|
||||||
color: statusColor,
|
warga != null && warga['nama_lengkap'] != null
|
||||||
fontSize: 24,
|
? warga['nama_lengkap']
|
||||||
),
|
.toString()
|
||||||
),
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: statusColor,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -1753,7 +1773,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
if (penerima.tanggalPenerimaan != null)
|
if (penerima.tanggalPenerimaan != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Penerimaan',
|
'Tanggal Penerimaan',
|
||||||
DateTimeHelper.formatDate(
|
FormatHelper.formatDateTime(
|
||||||
penerima.tanggalPenerimaan!)),
|
penerima.tanggalPenerimaan!)),
|
||||||
if (penerima.jumlahBantuan != null)
|
if (penerima.jumlahBantuan != null)
|
||||||
_buildInfoRow('Jumlah Bantuan',
|
_buildInfoRow('Jumlah Bantuan',
|
||||||
@ -1946,7 +1966,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
_buildInfoRow('Status', 'Batal Terlaksana'),
|
_buildInfoRow('Status', 'Batal Terlaksana'),
|
||||||
if (penyaluran.tanggalSelesai != null)
|
if (penyaluran.tanggalSelesai != null)
|
||||||
_buildInfoRow('Tanggal Pembatalan',
|
_buildInfoRow('Tanggal Pembatalan',
|
||||||
DateTimeHelper.formatDateTime(penyaluran.tanggalSelesai!)),
|
FormatHelper.formatDateTime(penyaluran.tanggalSelesai!)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'Alasan Pembatalan:',
|
'Alasan Pembatalan:',
|
||||||
@ -2126,7 +2146,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Laporan',
|
'Tanggal Laporan',
|
||||||
controller.laporan.value?.tanggalLaporan != null
|
controller.laporan.value?.tanggalLaporan != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
controller.laporan.value!.tanggalLaporan!)
|
controller.laporan.value!.tanggalLaporan!)
|
||||||
: '-',
|
: '-',
|
||||||
),
|
),
|
||||||
|
@ -198,7 +198,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
|||||||
'Tempat, Tanggal Lahir',
|
'Tempat, Tanggal Lahir',
|
||||||
warga?['tempat_lahir'] != null &&
|
warga?['tempat_lahir'] != null &&
|
||||||
warga?['tanggal_lahir'] != null
|
warga?['tanggal_lahir'] != null
|
||||||
? '${warga!['tempat_lahir']}, ${DateTimeHelper.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
|
? '${warga!['tempat_lahir']}, ${FormatHelper.formatDateTime(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
|
||||||
: 'Bogor, 2 Juni 1990'),
|
: 'Bogor, 2 Juni 1990'),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
||||||
@ -236,18 +236,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
|||||||
|
|
||||||
String tanggalWaktuPenyaluran = '';
|
String tanggalWaktuPenyaluran = '';
|
||||||
if (widget.tanggalPenyaluran != null) {
|
if (widget.tanggalPenyaluran != null) {
|
||||||
final tanggal = DateTimeHelper.formatDate(widget.tanggalPenyaluran!);
|
final tanggal = FormatHelper.formatDateTime(widget.tanggalPenyaluran!);
|
||||||
final waktuMulai = DateTimeHelper.formatTime(widget.tanggalPenyaluran!);
|
final waktuMulai = FormatHelper.formatTime(widget.tanggalPenyaluran!);
|
||||||
final waktuSelesai = DateTimeHelper.formatTime(
|
final waktuSelesai = FormatHelper.formatTime(
|
||||||
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
|
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
|
||||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||||
} else if (penerima.penyaluranBantuan != null &&
|
} else if (penerima.penyaluranBantuan != null &&
|
||||||
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
|
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
|
||||||
final tanggalPenyaluran =
|
final tanggalPenyaluran =
|
||||||
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
|
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
|
||||||
final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran);
|
final tanggal = FormatHelper.formatDateTime(tanggalPenyaluran);
|
||||||
final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran);
|
final waktuMulai = FormatHelper.formatTime(tanggalPenyaluran);
|
||||||
final waktuSelesai = DateTimeHelper.formatTime(
|
final waktuSelesai = FormatHelper.formatTime(
|
||||||
tanggalPenyaluran.add(const Duration(hours: 1)));
|
tanggalPenyaluran.add(const Duration(hours: 1)));
|
||||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||||
} else {
|
} else {
|
||||||
|
@ -44,7 +44,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
|||||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||||
final lastUpdate = DateTime
|
final lastUpdate = DateTime
|
||||||
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
|
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
|
||||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
@ -280,7 +280,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
|
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@ -320,7 +320,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
|||||||
// Format tanggal menggunakan DateTimeHelper
|
// Format tanggal menggunakan DateTimeHelper
|
||||||
String formattedDate = '';
|
String formattedDate = '';
|
||||||
if (item.tanggalPengaduan != null) {
|
if (item.tanggalPengaduan != null) {
|
||||||
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
|
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_ba
|
|||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
class PenitipanView extends GetView<PenitipanBantuanController> {
|
class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||||
@ -72,7 +73,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.pending_actions,
|
icon: Icons.pending_actions,
|
||||||
title: 'Menunggu',
|
title: 'Menunggu',
|
||||||
value: DateTimeHelper.formatNumber(
|
value: FormatHelper.formatNumber(
|
||||||
controller.jumlahMenunggu.value),
|
controller.jumlahMenunggu.value),
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
),
|
),
|
||||||
@ -82,7 +83,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.check_circle,
|
icon: Icons.check_circle,
|
||||||
title: 'Terverifikasi',
|
title: 'Terverifikasi',
|
||||||
value: DateTimeHelper.formatNumber(
|
value: FormatHelper.formatNumber(
|
||||||
controller.jumlahTerverifikasi.value),
|
controller.jumlahTerverifikasi.value),
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
@ -92,8 +93,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.cancel,
|
icon: Icons.cancel,
|
||||||
title: 'Ditolak',
|
title: 'Ditolak',
|
||||||
value: DateTimeHelper.formatNumber(
|
value:
|
||||||
controller.jumlahDitolak.value),
|
FormatHelper.formatNumber(controller.jumlahDitolak.value),
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -219,7 +220,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@ -360,7 +361,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
DateTimeHelper.formatDate(item.createdAt),
|
FormatHelper.formatDateTime(item.createdAt),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
@ -380,15 +381,27 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
|
||||||
radius: 20,
|
radius: 20,
|
||||||
child: Text(
|
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||||
donaturNama.substring(0, 1).toUpperCase(),
|
backgroundImage: item.donatur != null &&
|
||||||
style: TextStyle(
|
item.donatur!.fotoProfil != null &&
|
||||||
color: AppTheme.primaryColor,
|
item.donatur!.fotoProfil!.isNotEmpty
|
||||||
fontWeight: FontWeight.bold,
|
? NetworkImage(item.donatur!.fotoProfil!)
|
||||||
),
|
: null,
|
||||||
),
|
child: (item.donatur == null ||
|
||||||
|
item.donatur!.fotoProfil == null ||
|
||||||
|
item.donatur!.fotoProfil!.isEmpty)
|
||||||
|
? Text(
|
||||||
|
donaturNama.isNotEmpty
|
||||||
|
? donaturNama.substring(0, 1).toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -546,8 +559,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
isUang
|
isUang
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleSmall
|
.titleSmall
|
||||||
@ -947,7 +960,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
kategoriSatuan: kategoriSatuan,
|
kategoriSatuan: kategoriSatuan,
|
||||||
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
|
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
|
||||||
showFullScreenImage: (String imageUrl) {
|
showFullScreenImage: (String imageUrl) {
|
||||||
DetailPenitipanDialog.showFullScreenImage(context, imageUrl);
|
ShowImageDialog.showFullScreen(context, imageUrl);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -992,7 +1005,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final lastUpdate = controller.lastUpdateTime.value;
|
final lastUpdate = controller.lastUpdateTime.value;
|
||||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
|
||||||
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
|
|
||||||
class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||||
const PenyaluranView({super.key});
|
const PenyaluranView({super.key});
|
||||||
@ -41,13 +42,20 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: Column(
|
||||||
onPressed: () => Get.to(() => const TambahPenyaluranView()),
|
mainAxisSize: MainAxisSize.min,
|
||||||
backgroundColor: AppTheme.primaryColor,
|
children: [
|
||||||
icon: const Icon(Icons.add, color: Colors.white),
|
// Tombol untuk menambah jadwal penyaluran
|
||||||
label: const Text('Tambah Jadwal',
|
FloatingActionButton.extended(
|
||||||
style: TextStyle(color: Colors.white)),
|
heroTag: 'tambahJadwal',
|
||||||
elevation: 2,
|
onPressed: () => Get.to(() => const TambahPenyaluranView()),
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
icon: const Icon(Icons.add, color: Colors.white),
|
||||||
|
label: const Text('Tambah Jadwal',
|
||||||
|
style: TextStyle(color: Colors.white)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -76,6 +84,11 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
// Ringkasan jadwal
|
// Ringkasan jadwal
|
||||||
_buildJadwalSummary(Get.context!),
|
_buildJadwalSummary(Get.context!),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Tombol untuk mengelola lokasi penyaluran
|
||||||
|
_buildLokasiPenyaluranSection(),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Jadwal hari ini
|
// Jadwal hari ini
|
||||||
@ -224,4 +237,240 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Widget untuk menampilkan section lokasi penyaluran
|
||||||
|
Widget _buildLokasiPenyaluranSection() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: Colors.blue.shade100, width: 1),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Lokasi Penyaluran',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// Menampilkan dialog daftar lokasi penyaluran
|
||||||
|
_showLokasiPenyaluranDialog();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.map, size: 16),
|
||||||
|
label: const Text('Lihat Lokasi'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.blue,
|
||||||
|
side: BorderSide(color: Colors.blue.shade300),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran),
|
||||||
|
icon: const Icon(Icons.add_location, size: 16),
|
||||||
|
label: const Text('Tambah Lokasi Penyaluran Baru'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue.shade50,
|
||||||
|
foregroundColor: Colors.blue.shade700,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: BorderSide(color: Colors.blue.shade200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menampilkan dialog daftar lokasi penyaluran
|
||||||
|
void _showLokasiPenyaluranDialog() {
|
||||||
|
Get.dialog(
|
||||||
|
Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Daftar Lokasi Penyaluran',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: Get.height * 0.5,
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Obx(() {
|
||||||
|
if (controller.isLokasiLoading.value) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.lokasiPenyaluranCache.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_off,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Belum ada lokasi penyaluran',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
Get.toNamed(Routes.tambahLokasiPenyaluran);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_location),
|
||||||
|
label: const Text('Tambah Lokasi'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: controller.lokasiPenyaluranCache.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final lokasi = controller.lokasiPenyaluranCache.values
|
||||||
|
.elementAt(index);
|
||||||
|
final lokasiId = controller.lokasiPenyaluranCache.keys
|
||||||
|
.elementAt(index);
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
lokasi.nama,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (lokasi.alamat != null &&
|
||||||
|
lokasi.alamat!.isNotEmpty)
|
||||||
|
Text(lokasi.alamat!),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (lokasi.isLokasiTitip)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Lokasi Penitipan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.green.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
Get.toNamed(Routes.tambahLokasiPenyaluran);
|
||||||
|
},
|
||||||
|
child: const Text('Tambah Lokasi Baru'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
|||||||
case 4:
|
case 4:
|
||||||
return const Text('Stok Bantuan');
|
return const Text('Stok Bantuan');
|
||||||
default:
|
default:
|
||||||
return const Text('Petugas Desa');
|
return const Text('Dashboard');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -223,14 +223,23 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 40,
|
radius: 40,
|
||||||
backgroundColor: Colors.white70,
|
backgroundColor: Colors.white70,
|
||||||
backgroundImage: controller.profilePhotoUrl != null
|
backgroundImage: controller.profilePhotoUrl != null &&
|
||||||
|
controller.profilePhotoUrl!.isNotEmpty
|
||||||
? NetworkImage(controller.profilePhotoUrl!)
|
? NetworkImage(controller.profilePhotoUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: controller.profilePhotoUrl == null
|
child: (controller.profilePhotoUrl == null ||
|
||||||
? Icon(
|
controller.profilePhotoUrl!.isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
color: Colors.white,
|
controller.nama.isNotEmpty
|
||||||
size: 40,
|
? controller.nama
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 30,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -396,6 +405,16 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
|||||||
Get.toNamed('/profile');
|
Get.toNamed('/profile');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
activeIcon: Icons.info,
|
||||||
|
title: 'Tentang Kami',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Get.toNamed('/about');
|
||||||
|
},
|
||||||
|
),
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.logout,
|
icon: Icons.logout,
|
||||||
title: 'Keluar',
|
title: 'Keluar',
|
||||||
@ -411,7 +430,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
|
'© ${DateTime.now().year} DisalurKita',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
|
@ -43,7 +43,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
|||||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||||
final lastUpdate = DateTime.now();
|
final lastUpdate = DateTime.now();
|
||||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
@ -135,7 +135,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
|
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@ -154,9 +154,9 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
|||||||
// Format tanggal menggunakan DateTimeHelper
|
// Format tanggal menggunakan DateTimeHelper
|
||||||
String formattedDate = '';
|
String formattedDate = '';
|
||||||
if (item.tanggalPengaduan != null) {
|
if (item.tanggalPengaduan != null) {
|
||||||
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
|
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
|
||||||
} else if (item.createdAt != null) {
|
} else if (item.createdAt != null) {
|
||||||
formattedDate = DateTimeHelper.formatDate(item.createdAt);
|
formattedDate = FormatHelper.formatDateTime(item.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color statusColor = AppTheme.successColor;
|
Color statusColor = AppTheme.successColor;
|
||||||
|
@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||||
const RiwayatPenitipanView({super.key});
|
const RiwayatPenitipanView({super.key});
|
||||||
@ -47,7 +48,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||||
final tanggal =
|
final tanggal =
|
||||||
DateTimeHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
|
FormatHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
|
||||||
|
|
||||||
return donaturNama.contains(searchText) ||
|
return donaturNama.contains(searchText) ||
|
||||||
kategoriNama.contains(searchText) ||
|
kategoriNama.contains(searchText) ||
|
||||||
@ -99,7 +100,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
@ -113,7 +114,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
|
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
@ -126,7 +127,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
size: 16, color: Colors.grey[600]),
|
size: 16, color: Colors.grey[600]),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Update: ${DateTimeHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
|
'Update: ${FormatHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -262,7 +263,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
DateTimeHelper.formatDate(item.createdAt),
|
FormatHelper.formatDateTime(item.createdAt),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
@ -282,17 +283,26 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
|
||||||
radius: 20,
|
radius: 20,
|
||||||
child: Text(
|
backgroundColor: statusColor.withOpacity(0.2),
|
||||||
donaturNama.isNotEmpty
|
backgroundImage: item.donatur != null &&
|
||||||
? donaturNama.substring(0, 1).toUpperCase()
|
item.donatur!.fotoProfil != null &&
|
||||||
: '?',
|
item.donatur!.fotoProfil!.isNotEmpty
|
||||||
style: TextStyle(
|
? NetworkImage(item.donatur!.fotoProfil!)
|
||||||
color: AppTheme.primaryColor,
|
: null,
|
||||||
fontWeight: FontWeight.bold,
|
child: (item.donatur == null ||
|
||||||
),
|
item.donatur!.fotoProfil == null ||
|
||||||
),
|
item.donatur!.fotoProfil!.isEmpty)
|
||||||
|
? Text(
|
||||||
|
donaturNama.isNotEmpty
|
||||||
|
? donaturNama.substring(0, 1).toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: statusColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -422,8 +432,8 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
isUang
|
isUang
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleSmall
|
.titleSmall
|
||||||
@ -579,20 +589,20 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Jumlah',
|
'Jumlah',
|
||||||
isUang
|
isUang
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
|
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
|
||||||
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
|
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
|
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Tanggal Penitipan',
|
'Tanggal Penitipan',
|
||||||
DateTimeHelper.formatDateTime(item.tanggalPenitipan,
|
FormatHelper.formatDateTime(item.tanggalPenitipan,
|
||||||
defaultValue: 'Tidak ada tanggal'),
|
defaultValue: 'Tidak ada tanggal'),
|
||||||
),
|
),
|
||||||
if (item.tanggalVerifikasi != null)
|
if (item.tanggalVerifikasi != null)
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Tanggal Verifikasi',
|
'Tanggal Verifikasi',
|
||||||
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
|
FormatHelper.formatDateTime(item.tanggalVerifikasi),
|
||||||
),
|
),
|
||||||
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
|
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
@ -600,7 +610,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
controller.getPetugasDesaNama(item.petugasDesaId),
|
controller.getPetugasDesaNama(item.petugasDesaId),
|
||||||
),
|
),
|
||||||
_buildDetailItem('Tanggal Dibuat',
|
_buildDetailItem('Tanggal Dibuat',
|
||||||
DateTimeHelper.formatDateTime(item.createdAt)),
|
FormatHelper.formatDateTime(item.createdAt)),
|
||||||
if (item.alasanPenolakan != null &&
|
if (item.alasanPenolakan != null &&
|
||||||
item.alasanPenolakan!.isNotEmpty)
|
item.alasanPenolakan!.isNotEmpty)
|
||||||
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
|
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
|
||||||
@ -626,8 +636,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showFullScreenImage(
|
ShowImageDialog.show(
|
||||||
context, item.fotoBantuan![index]);
|
context,
|
||||||
|
item.fotoBantuan![index],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
@ -677,8 +689,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showFullScreenImage(
|
ShowImageDialog.show(
|
||||||
context, item.fotoBantuan![index]);
|
context,
|
||||||
|
item.fotoBantuan![index],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
@ -721,8 +735,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showFullScreenImage(
|
ShowImageDialog.show(
|
||||||
context, item.fotoBuktiSerahTerima!);
|
context,
|
||||||
|
item.fotoBuktiSerahTerima!,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@ -757,58 +773,6 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showFullScreenImage(BuildContext context, String imageUrl) {
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
child: Image.network(
|
|
||||||
imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.error,
|
|
||||||
size: 50,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.5),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailItem(String label, String value) {
|
Widget _buildDetailItem(String label, String value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
@ -52,7 +52,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
.getKategoriBantuanName(item.kategoriBantuanId)
|
.getKategoriBantuanName(item.kategoriBantuanId)
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
final tanggal =
|
final tanggal =
|
||||||
DateTimeHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
|
FormatHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
|
||||||
|
|
||||||
return nama.contains(searchText) ||
|
return nama.contains(searchText) ||
|
||||||
deskripsi.contains(searchText) ||
|
deskripsi.contains(searchText) ||
|
||||||
@ -105,7 +105,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
@ -119,7 +119,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
|
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
@ -132,7 +132,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
size: 16, color: Colors.grey[600]),
|
size: 16, color: Colors.grey[600]),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Update: ${DateTimeHelper.formatDateTimeWithHour(DateTime.now())}',
|
'Update: ${FormatHelper.formatDateTimeWithHour(DateTime.now())}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -305,7 +305,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
child: _buildInfoItem(
|
child: _buildInfoItem(
|
||||||
Icons.event,
|
Icons.event,
|
||||||
'Tanggal',
|
'Tanggal',
|
||||||
DateTimeHelper.formatDateTime(item.tanggalPenyaluran,
|
FormatHelper.formatDateTime(item.tanggalPenyaluran,
|
||||||
format: 'dd MMM yyyy HH:mm'),
|
format: 'dd MMM yyyy HH:mm'),
|
||||||
Theme.of(context).textTheme,
|
Theme.of(context).textTheme,
|
||||||
),
|
),
|
||||||
@ -316,17 +316,57 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
|||||||
_buildInfoItem(
|
_buildInfoItem(
|
||||||
Icons.people_outline,
|
Icons.people_outline,
|
||||||
'Jumlah Penerima',
|
'Jumlah Penerima',
|
||||||
'${DateTimeHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
|
'${FormatHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
|
||||||
Theme.of(context).textTheme,
|
Theme.of(context).textTheme,
|
||||||
),
|
),
|
||||||
if (item.alasanPembatalan != null &&
|
if (item.alasanPembatalan != null &&
|
||||||
item.alasanPembatalan!.isNotEmpty) ...[
|
item.alasanPembatalan!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
_buildInfoItem(
|
Container(
|
||||||
Icons.info_outline,
|
padding: const EdgeInsets.all(10),
|
||||||
'Alasan Pembatalan',
|
decoration: BoxDecoration(
|
||||||
item.alasanPembatalan!,
|
color: Colors.red.shade50,
|
||||||
Theme.of(context).textTheme,
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.shade200),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cancel_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Alasan Pembatalan',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
item.alasanPembatalan!,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Colors.red.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
@ -6,6 +6,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok
|
|||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class RiwayatStokView extends GetView<RiwayatStokController> {
|
class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||||
const RiwayatStokView({super.key});
|
const RiwayatStokView({super.key});
|
||||||
@ -353,7 +354,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@ -543,7 +544,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
riwayat.createdAt != null
|
riwayat.createdAt != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
riwayat.createdAt!)
|
riwayat.createdAt!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -598,7 +599,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
padding: const EdgeInsets.only(left: 44),
|
padding: const EdgeInsets.only(left: 44),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
_showImageDialog(context, riwayat.fotoBukti!),
|
ShowImageDialog.show(context, riwayat.fotoBukti!),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withOpacity(0.1),
|
color: Colors.blue.withOpacity(0.1),
|
||||||
@ -704,97 +705,6 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showImageDialog(BuildContext context, String imageUrl) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Dialog(
|
|
||||||
insetPadding: const EdgeInsets.all(16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
title: const Text(
|
|
||||||
'Bukti Foto',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: AppTheme.primaryColor,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.5,
|
|
||||||
child: InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
boundaryMargin: const EdgeInsets.all(16),
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
placeholder: (context, url) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error, color: Colors.red, size: 48),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Text(
|
|
||||||
'Gagal memuat gambar: $error',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.zoom_in, size: 20, color: Colors.grey),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Cubit untuk memperbesar/memperkecil',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
|
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
|
||||||
// Reset form
|
// Reset form
|
||||||
controller.resetForm();
|
controller.resetForm();
|
||||||
@ -1152,7 +1062,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
Widget _buildPenitipanDetail(
|
Widget _buildPenitipanDetail(
|
||||||
BuildContext context, Map<String, dynamic> data) {
|
BuildContext context, Map<String, dynamic> data) {
|
||||||
final String tanggal = data['created_at'] != null
|
final String tanggal = data['created_at'] != null
|
||||||
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
|
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
|
||||||
: '-';
|
: '-';
|
||||||
|
|
||||||
final String namaPenitip = data['donatur'] != null
|
final String namaPenitip = data['donatur'] != null
|
||||||
@ -1357,7 +1267,8 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: index < fotoBantuan.length - 1 ? 8.0 : 0),
|
right: index < fotoBantuan.length - 1 ? 8.0 : 0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _showImageDialog(context, imageUrl),
|
onTap: () =>
|
||||||
|
ShowImageDialog.show(context, imageUrl),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 200,
|
width: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -1442,7 +1353,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
Widget _buildPenerimaanDetail(
|
Widget _buildPenerimaanDetail(
|
||||||
BuildContext context, Map<String, dynamic> data) {
|
BuildContext context, Map<String, dynamic> data) {
|
||||||
final String tanggal = data['created_at'] != null
|
final String tanggal = data['created_at'] != null
|
||||||
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
|
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
|
||||||
: '-';
|
: '-';
|
||||||
|
|
||||||
final String namaPenerima = data['warga'] != null
|
final String namaPenerima = data['warga'] != null
|
||||||
@ -1646,7 +1557,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => _showImageDialog(context, buktiPenerimaan),
|
onTap: () => ShowImageDialog.show(context, buktiPenerimaan),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 180,
|
height: 180,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
@ -156,7 +156,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Rp ${DateTimeHelper.formatNumber(controller.totalDanaBantuan.value)}',
|
'Rp ${FormatHelper.formatNumber(controller.totalDanaBantuan.value)}',
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -512,8 +512,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
item.isUang == true
|
item.isUang == true
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
|
? 'Rp ${FormatHelper.formatNumber(item.totalStok)}'
|
||||||
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
|
: '${FormatHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
@ -549,7 +549,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.updatedAt != null
|
item.updatedAt != null
|
||||||
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}'
|
? 'Diperbarui: ${FormatHelper.formatDateTimeWithHour(item.updatedAt!)}'
|
||||||
: 'Tidak ada data pembaruan',
|
: 'Tidak ada data pembaruan',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -984,8 +984,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
isUang
|
isUang
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
|
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
|
||||||
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1175,8 +1175,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
stok.isUang == true
|
stok.isUang == true
|
||||||
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
|
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
|
||||||
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1240,7 +1240,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final lastUpdate = controller.lastUpdateTime.value;
|
final lastUpdate = controller.lastUpdateTime.value;
|
||||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||||
|
|
||||||
|
class TambahLokasiPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||||
|
const TambahLokasiPenyaluranView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Tambah Lokasi Penyaluran'),
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: _buildTambahLokasiPenyaluranForm(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTambahLokasiPenyaluranForm(BuildContext context) {
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController namaController = TextEditingController();
|
||||||
|
final TextEditingController alamatLengkapController =
|
||||||
|
TextEditingController();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Judul Form
|
||||||
|
Text(
|
||||||
|
'Formulir Lokasi Penyaluran',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Nama Lokasi
|
||||||
|
Text(
|
||||||
|
'Nama Lokasi',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: namaController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan nama lokasi penyaluran',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nama lokasi tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Alamat Lengkap
|
||||||
|
Text(
|
||||||
|
'Alamat Lengkap',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: alamatLengkapController,
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Masukkan alamat lengkap lokasi',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Alamat lengkap tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Tombol Submit
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
// Panggil fungsi untuk menambahkan lokasi penyaluran
|
||||||
|
_tambahLokasiPenyaluran(
|
||||||
|
nama: namaController.text,
|
||||||
|
alamatLengkap: alamatLengkapController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Simpan Lokasi',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tambahLokasiPenyaluran({
|
||||||
|
required String nama,
|
||||||
|
required String alamatLengkap,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Tampilkan loading
|
||||||
|
Get.dialog(
|
||||||
|
const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate UUID untuk ID lokasi
|
||||||
|
final uuid = const Uuid();
|
||||||
|
final String id = uuid.v4();
|
||||||
|
|
||||||
|
// Ambil ID petugas desa yang sedang login dari controller
|
||||||
|
final String? petugasDesaId = controller.supabaseService.currentUser?.id;
|
||||||
|
|
||||||
|
if (petugasDesaId == null) {
|
||||||
|
Get.back(); // Tutup dialog loading
|
||||||
|
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Sesi login tidak valid. Silakan login kembali.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan desa_id dari data petugas desa
|
||||||
|
// Ambil data petugas desa dari Supabase untuk mendapatkan desa_id
|
||||||
|
final petugasDesaData = await controller.supabaseService.client
|
||||||
|
.from('petugas_desa')
|
||||||
|
.select('desa_id')
|
||||||
|
.eq('id', petugasDesaId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
final String? desaId = petugasDesaData['desa_id'];
|
||||||
|
|
||||||
|
if (desaId == null) {
|
||||||
|
Get.back(); // Tutup dialog loading
|
||||||
|
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Data desa tidak ditemukan. Silakan hubungi administrator.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data untuk insert
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
'id': id,
|
||||||
|
'nama': nama,
|
||||||
|
'alamat_lengkap': alamatLengkap,
|
||||||
|
'desa_id': desaId,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert data ke tabel lokasi_penyaluran
|
||||||
|
await controller.supabaseService.client
|
||||||
|
.from('lokasi_penyaluran')
|
||||||
|
.insert(data);
|
||||||
|
|
||||||
|
// Tutup dialog loading
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
// Tampilkan pesan sukses
|
||||||
|
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Lokasi penyaluran berhasil ditambahkan'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kembali ke halaman sebelumnya
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
// Refresh data di controller
|
||||||
|
controller.refreshData();
|
||||||
|
} catch (e) {
|
||||||
|
// Tutup dialog loading
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
// Tampilkan pesan error
|
||||||
|
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Gagal menambahkan lokasi penyaluran: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -231,12 +231,39 @@ class ProfileView extends GetView<ProfileController> {
|
|||||||
Widget _buildDefaultProfileImage() {
|
Widget _buildDefaultProfileImage() {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||||
child: const Icon(
|
child: Obx(() {
|
||||||
Icons.person,
|
final user = controller.user.value;
|
||||||
size: 70,
|
final roleData = controller.roleData.value;
|
||||||
color: AppTheme.primaryColor,
|
|
||||||
),
|
String displayInitial = '?';
|
||||||
|
|
||||||
|
if (roleData != null && roleData.isNotEmpty) {
|
||||||
|
final roleDataValue = roleData;
|
||||||
|
if (roleDataValue['nama_lengkap'] != null &&
|
||||||
|
roleDataValue['nama_lengkap'].toString().isNotEmpty) {
|
||||||
|
displayInitial = roleDataValue['nama_lengkap']
|
||||||
|
.toString()
|
||||||
|
.substring(0, 1)
|
||||||
|
.toUpperCase();
|
||||||
|
} else if (roleDataValue['nama'] != null &&
|
||||||
|
roleDataValue['nama'].toString().isNotEmpty) {
|
||||||
|
displayInitial =
|
||||||
|
roleDataValue['nama'].toString().substring(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
} else if (user != null && user.name != null && user.name!.isNotEmpty) {
|
||||||
|
displayInitial = user.name!.substring(0, 1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
displayInitial,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 60,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,23 +33,20 @@ class _SplashViewState extends State<SplashView> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: AppTheme.primaryGradient,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/images/logo.png',
|
'assets/images/logo-disalurkita.png',
|
||||||
width: 120,
|
width: 150,
|
||||||
height: 120,
|
height: 150,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: AppTheme.primaryColor,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@ -62,24 +59,25 @@ class _SplashViewState extends State<SplashView> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
const Text(
|
||||||
'Aplikasi Penyaluran',
|
'DisalurKita',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: AppTheme.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'Bantuan Sosial',
|
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: AppTheme.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
const CircularProgressIndicator(
|
const CircularProgressIndicator(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
|
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:timeline_tile/timeline_tile.dart';
|
import 'package:timeline_tile/timeline_tile.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
|
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
|
||||||
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
|
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||||
|
|
||||||
class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||||
const WargaDetailPengaduanView({super.key});
|
const WargaDetailPengaduanView({super.key});
|
||||||
@ -670,8 +670,7 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
pengaduan.tanggalPengaduan != null
|
pengaduan.tanggalPengaduan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||||
.format(pengaduan.tanggalPengaduan!)
|
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
@ -1309,8 +1308,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: tindakan.buktiTindakan!.map((bukti) {
|
children: tindakan.buktiTindakan!.map((bukti) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => ShowImageDialog.showFullScreen(
|
||||||
showFullScreenImage(context, bukti),
|
context, bukti),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -1407,8 +1406,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tindakan.tanggalTindakan != null
|
tindakan.tanggalTindakan != null
|
||||||
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
|
? FormatHelper.formatDateTime(
|
||||||
.format(tindakan.tanggalTindakan!)
|
tindakan.tanggalTindakan!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -1429,183 +1428,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showFullScreenImage(BuildContext context, String imageUrl) {
|
void _showFullScreenImage(BuildContext context, String imagePath) {
|
||||||
// Buat controller untuk InteractiveViewer
|
ShowImageDialog.showFullScreen(context, imagePath);
|
||||||
final TransformationController transformationController =
|
|
||||||
TransformationController();
|
|
||||||
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
insetPadding: EdgeInsets.zero,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Colors.black,
|
|
||||||
child: InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
transformationController: transformationController,
|
|
||||||
child: Center(
|
|
||||||
child: Hero(
|
|
||||||
tag: imageUrl,
|
|
||||||
child: imageUrl.startsWith('http')
|
|
||||||
? Image.network(
|
|
||||||
imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
Colors.white),
|
|
||||||
value: loadingProgress.expectedTotalBytes !=
|
|
||||||
null
|
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.broken_image,
|
|
||||||
size: 60,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Gagal memuat gambar',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Image.file(
|
|
||||||
File(imageUrl),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.broken_image,
|
|
||||||
size: 60,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Gagal memuat gambar',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.6),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 20,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildImageControlButton(
|
|
||||||
icon: Icons.zoom_out,
|
|
||||||
onTap: () {
|
|
||||||
// Zoom out
|
|
||||||
final Matrix4 matrix =
|
|
||||||
transformationController.value.clone();
|
|
||||||
matrix.scale(0.75);
|
|
||||||
transformationController.value = matrix;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
_buildImageControlButton(
|
|
||||||
icon: Icons.refresh,
|
|
||||||
onTap: () {
|
|
||||||
// Reset
|
|
||||||
transformationController.value = Matrix4.identity();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
_buildImageControlButton(
|
|
||||||
icon: Icons.zoom_in,
|
|
||||||
onTap: () {
|
|
||||||
// Zoom in
|
|
||||||
final Matrix4 matrix =
|
|
||||||
transformationController.value.clone();
|
|
||||||
matrix.scale(1.5);
|
|
||||||
transformationController.value = matrix;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImageControlButton({
|
|
||||||
required IconData icon,
|
|
||||||
required Function() onTap,
|
|
||||||
}) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.6),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
icon,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2056,8 +1880,7 @@ class _TambahTindakanPengaduanViewState
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showFullScreenImage(BuildContext context, String imagePath) {
|
void _showFullScreenImage(BuildContext context, String imagePath) {
|
||||||
final wargaDetailView = Get.find<WargaDetailPengaduanView>();
|
ShowImageDialog.showFullScreen(context, imagePath);
|
||||||
wargaDetailView.showFullScreenImage(context, imagePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _simpanTindakan() async {
|
Future<void> _simpanTindakan() async {
|
||||||
@ -2078,22 +1901,6 @@ class _TambahTindakanPengaduanViewState
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Di sini kita baru melakukan upload file ke server
|
|
||||||
// Contoh implementasi:
|
|
||||||
|
|
||||||
// 1. Upload semua file bukti tindakan
|
|
||||||
// final List<String> buktiTindakanUrls = await uploadMultipleFiles(buktiTindakanPaths);
|
|
||||||
|
|
||||||
// 2. Simpan data tindakan ke database
|
|
||||||
// await saveTindakanPengaduan(
|
|
||||||
// pengaduanId: widget.pengaduanId,
|
|
||||||
// kategoriTindakan: selectedKategori!,
|
|
||||||
// prioritas: selectedPrioritas!,
|
|
||||||
// tindakan: tindakanController.text,
|
|
||||||
// catatan: catatanController.text,
|
|
||||||
// buktiTindakanUrls: buktiTindakanUrls,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Tampilkan pesan sukses
|
// Tampilkan pesan sukses
|
||||||
Get.back(); // Kembali ke halaman sebelumnya
|
Get.back(); // Kembali ke halaman sebelumnya
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
|
@ -11,12 +11,12 @@ class FormPengaduanView extends StatefulWidget {
|
|||||||
final List<File>? selectedImages;
|
final List<File>? selectedImages;
|
||||||
|
|
||||||
const FormPengaduanView({
|
const FormPengaduanView({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.uidPenerimaan,
|
required this.uidPenerimaan,
|
||||||
this.judul,
|
this.judul,
|
||||||
this.deskripsi,
|
this.deskripsi,
|
||||||
this.selectedImages,
|
this.selectedImages,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FormPengaduanView> createState() => _FormPengaduanViewState();
|
State<FormPengaduanView> createState() => _FormPengaduanViewState();
|
||||||
@ -219,7 +219,7 @@ class _FormPengaduanViewState extends State<FormPengaduanView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
SizedBox(
|
||||||
height: 120,
|
height: 120,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||||
|
|
||||||
class WargaDashboardView extends GetView<WargaDashboardController> {
|
class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||||
@ -23,6 +23,54 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Header DisalurKita dengan logo dan slogan
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo-disalurkita.png',
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
const Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'DisalurKita',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
_buildWelcomeSection(),
|
_buildWelcomeSection(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildStatisticSection(),
|
_buildStatisticSection(),
|
||||||
@ -90,10 +138,17 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
? NetworkImage(controller.profilePhotoUrl!)
|
? NetworkImage(controller.profilePhotoUrl!)
|
||||||
: null,
|
: null,
|
||||||
child: controller.profilePhotoUrl == null
|
child: controller.profilePhotoUrl == null
|
||||||
? Icon(
|
? Text(
|
||||||
Icons.person,
|
controller.nama.isNotEmpty
|
||||||
color: Colors.blue.shade700,
|
? controller.nama
|
||||||
size: 30,
|
.substring(0, 1)
|
||||||
|
.toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -417,12 +472,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPenerimaanSummary() {
|
Widget _buildPenerimaanSummary() {
|
||||||
final currencyFormat = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
double totalUang = 0;
|
double totalUang = 0;
|
||||||
Map<String, double> totalNonUang = {};
|
Map<String, double> totalNonUang = {};
|
||||||
|
|
||||||
@ -494,7 +543,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
icon: Icons.attach_money,
|
icon: Icons.attach_money,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
title: 'Total Bantuan Uang',
|
title: 'Total Bantuan Uang',
|
||||||
value: currencyFormat.format(totalUang),
|
value: FormatHelper.formatRupiah(totalUang),
|
||||||
),
|
),
|
||||||
if (totalNonUang.isNotEmpty) ...[
|
if (totalNonUang.isNotEmpty) ...[
|
||||||
if (totalUang > 0)
|
if (totalUang > 0)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -131,17 +131,11 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) {
|
Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) {
|
||||||
final currencyFormat = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
||||||
String formattedJumlah = '';
|
String formattedJumlah = '';
|
||||||
if (penyaluran.jumlahBantuan != null) {
|
if (penyaluran.jumlahBantuan != null) {
|
||||||
if (penyaluran.isUang == true) {
|
if (penyaluran.isUang == true) {
|
||||||
formattedJumlah = currencyFormat.format(penyaluran.jumlahBantuan);
|
formattedJumlah = FormatHelper.formatRupiah(penyaluran.jumlahBantuan);
|
||||||
} else {
|
} else {
|
||||||
formattedJumlah =
|
formattedJumlah =
|
||||||
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
|
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
|
||||||
@ -390,8 +384,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
title: 'Tanggal Penerimaan',
|
title: 'Tanggal Penerimaan',
|
||||||
value: penyaluran.tanggalPenerimaan != null
|
value: penyaluran.tanggalPenerimaan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||||
.format(penyaluran.tanggalPenerimaan!)
|
|
||||||
: 'Belum diterima',
|
: 'Belum diterima',
|
||||||
statusColor: null,
|
statusColor: null,
|
||||||
),
|
),
|
||||||
@ -400,8 +393,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
title: 'Waktu Penerimaan',
|
title: 'Waktu Penerimaan',
|
||||||
value: penyaluran.tanggalPenerimaan != null
|
value: penyaluran.tanggalPenerimaan != null
|
||||||
? DateFormat('HH:mm', 'id_ID')
|
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||||
.format(penyaluran.tanggalPenerimaan!)
|
|
||||||
: 'Belum diterima',
|
: 'Belum diterima',
|
||||||
statusColor: null,
|
statusColor: null,
|
||||||
),
|
),
|
||||||
@ -758,8 +750,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
icon: Icons.update,
|
icon: Icons.update,
|
||||||
title: 'Terakhir Diperbarui',
|
title: 'Terakhir Diperbarui',
|
||||||
value: penyaluran.tanggalPenerimaan != null
|
value: penyaluran.tanggalPenerimaan != null
|
||||||
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||||
.format(penyaluran.tanggalPenerimaan!)
|
|
||||||
: 'Tidak tersedia',
|
: 'Tidak tersedia',
|
||||||
statusColor: null,
|
statusColor: null,
|
||||||
),
|
),
|
||||||
@ -1394,7 +1385,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'Pengaduan Terdaftar',
|
'Pengaduan',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -1547,8 +1538,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
pengaduan.tanggalPengaduan != null
|
pengaduan.tanggalPengaduan != null
|
||||||
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||||
.format(pengaduan.tanggalPengaduan!)
|
|
||||||
: 'Tanggal tidak tersedia',
|
: 'Tanggal tidak tersedia',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
|
||||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||||
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart';
|
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
|
||||||
class WargaPengaduanView extends GetView<WargaDashboardController> {
|
class WargaPengaduanView extends GetView<WargaDashboardController> {
|
||||||
const WargaPengaduanView({super.key});
|
const WargaPengaduanView({super.key});
|
||||||
@ -380,7 +375,7 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.tanggalPengaduan != null
|
item.tanggalPengaduan != null
|
||||||
? DateTimeHelper.formatDateTime(
|
? FormatHelper.formatDateTime(
|
||||||
item.tanggalPengaduan!)
|
item.tanggalPengaduan!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -20,13 +20,13 @@ class WargaView extends GetView<WargaDashboardController> {
|
|||||||
title: Obx(() {
|
title: Obx(() {
|
||||||
switch (controller.activeTabIndex.value) {
|
switch (controller.activeTabIndex.value) {
|
||||||
case 0:
|
case 0:
|
||||||
return const Text('Dashboard Warga');
|
return const Text('Dashboard');
|
||||||
case 1:
|
case 1:
|
||||||
return const Text('Penerimaan Bantuan');
|
return const Text('Penerimaan Bantuan');
|
||||||
case 2:
|
case 2:
|
||||||
return const Text('Pengaduan');
|
return const Text('Pengaduan');
|
||||||
default:
|
default:
|
||||||
return const Text('Dashboard Warga');
|
return const Text('Dashboard');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -164,16 +164,19 @@ class WargaView extends GetView<WargaDashboardController> {
|
|||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 40,
|
radius: 40,
|
||||||
backgroundColor: Colors.white70,
|
backgroundColor: Colors.white70,
|
||||||
backgroundImage: controller.profilePhotoUrl != null &&
|
backgroundImage: controller.fotoProfil.value.isNotEmpty
|
||||||
controller.profilePhotoUrl!.isNotEmpty
|
? NetworkImage(controller.fotoProfil.value)
|
||||||
? NetworkImage(controller.profilePhotoUrl!)
|
|
||||||
: null,
|
: null,
|
||||||
child: controller.profilePhotoUrl == null ||
|
child: controller.fotoProfil.isEmpty
|
||||||
controller.profilePhotoUrl!.isEmpty
|
? Text(
|
||||||
? Icon(
|
controller.nama.isNotEmpty
|
||||||
Icons.person,
|
? controller.nama.substring(0, 1).toUpperCase()
|
||||||
color: Colors.white,
|
: '?',
|
||||||
size: 40,
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -292,6 +295,15 @@ class WargaView extends GetView<WargaDashboardController> {
|
|||||||
controller.refreshData();
|
controller.refreshData();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
activeIcon: Icons.info,
|
||||||
|
title: 'Tentang Kami',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Get.toNamed('/about');
|
||||||
|
},
|
||||||
|
),
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.logout,
|
icon: Icons.logout,
|
||||||
title: 'Keluar',
|
title: 'Keluar',
|
||||||
@ -307,7 +319,7 @@ class WargaView extends GetView<WargaDashboardController> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
|
'© ${DateTime.now().year} DisalurKita',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/auth/views/forgot_password_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/views/login_view.dart';
|
import 'package:penyaluran_app/app/modules/auth/views/login_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/views/register_donatur_view.dart';
|
import 'package:penyaluran_app/app/modules/auth/views/register_donatur_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart';
|
import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart';
|
||||||
@ -11,6 +12,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penitipan_
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart';
|
||||||
@ -18,7 +20,8 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_pengaduan_
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/about/views/about_view.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/about/bindings/about_binding.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart';
|
||||||
import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart';
|
import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart';
|
||||||
@ -64,6 +67,11 @@ class AppPages {
|
|||||||
page: () => const RegisterDonaturView(),
|
page: () => const RegisterDonaturView(),
|
||||||
binding: AuthBinding(),
|
binding: AuthBinding(),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.forgotPassword,
|
||||||
|
page: () => const ForgotPasswordView(),
|
||||||
|
binding: AuthBinding(),
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.wargaDashboard,
|
name: Routes.wargaDashboard,
|
||||||
page: () => WargaView(),
|
page: () => WargaView(),
|
||||||
@ -92,6 +100,11 @@ class AppPages {
|
|||||||
page: () => const PetugasDesaView(),
|
page: () => const PetugasDesaView(),
|
||||||
binding: PetugasDesaBinding(),
|
binding: PetugasDesaBinding(),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.about,
|
||||||
|
page: () => const AboutView(),
|
||||||
|
binding: AboutBinding(),
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.permintaanPenjadwalan,
|
name: _Paths.permintaanPenjadwalan,
|
||||||
page: () => const PermintaanPenjadwalanView(),
|
page: () => const PermintaanPenjadwalanView(),
|
||||||
@ -137,6 +150,11 @@ class AppPages {
|
|||||||
page: () => const TambahPenyaluranView(),
|
page: () => const TambahPenyaluranView(),
|
||||||
binding: PetugasDesaBinding(),
|
binding: PetugasDesaBinding(),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.tambahLokasiPenyaluran,
|
||||||
|
page: () => const TambahLokasiPenyaluranView(),
|
||||||
|
binding: PetugasDesaBinding(),
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.detailPenyaluran,
|
name: _Paths.detailPenyaluran,
|
||||||
page: () => DetailPenyaluranPage(),
|
page: () => DetailPenyaluranPage(),
|
||||||
|
@ -6,6 +6,7 @@ abstract class Routes {
|
|||||||
static const login = _Paths.login;
|
static const login = _Paths.login;
|
||||||
static const register = _Paths.register;
|
static const register = _Paths.register;
|
||||||
static const registerDonatur = _Paths.registerDonatur;
|
static const registerDonatur = _Paths.registerDonatur;
|
||||||
|
static const forgotPassword = _Paths.forgotPassword;
|
||||||
static const wargaDashboard = _Paths.wargaDashboard;
|
static const wargaDashboard = _Paths.wargaDashboard;
|
||||||
static const wargaPenerimaan = _Paths.wargaPenerimaan;
|
static const wargaPenerimaan = _Paths.wargaPenerimaan;
|
||||||
static const wargaPengaduan = _Paths.wargaPengaduan;
|
static const wargaPengaduan = _Paths.wargaPengaduan;
|
||||||
@ -23,10 +24,12 @@ abstract class Routes {
|
|||||||
static const konfirmasiPenerima = _Paths.konfirmasiPenerima;
|
static const konfirmasiPenerima = _Paths.konfirmasiPenerima;
|
||||||
static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran;
|
static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran;
|
||||||
static const profile = _Paths.profile;
|
static const profile = _Paths.profile;
|
||||||
|
static const about = _Paths.about;
|
||||||
static const riwayatPenitipan = _Paths.riwayatPenitipan;
|
static const riwayatPenitipan = _Paths.riwayatPenitipan;
|
||||||
static const daftarDonatur = _Paths.daftarDonatur;
|
static const daftarDonatur = _Paths.daftarDonatur;
|
||||||
static const detailDonatur = _Paths.detailDonatur;
|
static const detailDonatur = _Paths.detailDonatur;
|
||||||
static const tambahPenyaluran = _Paths.tambahPenyaluran;
|
static const tambahPenyaluran = _Paths.tambahPenyaluran;
|
||||||
|
static const tambahLokasiPenyaluran = _Paths.tambahLokasiPenyaluran;
|
||||||
static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran;
|
static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran;
|
||||||
static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran;
|
static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran;
|
||||||
static const laporanPenyaluran = _Paths.laporanPenyaluran;
|
static const laporanPenyaluran = _Paths.laporanPenyaluran;
|
||||||
@ -51,6 +54,7 @@ abstract class _Paths {
|
|||||||
static const login = '/login';
|
static const login = '/login';
|
||||||
static const register = '/register';
|
static const register = '/register';
|
||||||
static const registerDonatur = '/register-donatur';
|
static const registerDonatur = '/register-donatur';
|
||||||
|
static const forgotPassword = '/forgot-password';
|
||||||
static const wargaDashboard = '/warga-dashboard';
|
static const wargaDashboard = '/warga-dashboard';
|
||||||
static const wargaPenerimaan = '/warga-penerimaan';
|
static const wargaPenerimaan = '/warga-penerimaan';
|
||||||
static const wargaPengaduan = '/warga-pengaduan';
|
static const wargaPengaduan = '/warga-pengaduan';
|
||||||
@ -68,10 +72,12 @@ abstract class _Paths {
|
|||||||
static const konfirmasiPenerima = '/daftar-penerima/konfirmasi';
|
static const konfirmasiPenerima = '/daftar-penerima/konfirmasi';
|
||||||
static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran';
|
static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran';
|
||||||
static const profile = '/profile';
|
static const profile = '/profile';
|
||||||
|
static const about = '/about';
|
||||||
static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan';
|
static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan';
|
||||||
static const daftarDonatur = '/daftar-donatur';
|
static const daftarDonatur = '/daftar-donatur';
|
||||||
static const detailDonatur = '/daftar-donatur/detail';
|
static const detailDonatur = '/daftar-donatur/detail';
|
||||||
static const tambahPenyaluran = '/tambah-penyaluran';
|
static const tambahPenyaluran = '/tambah-penyaluran';
|
||||||
|
static const tambahLokasiPenyaluran = '/tambah-lokasi-penyaluran';
|
||||||
static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran';
|
static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran';
|
||||||
static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran';
|
static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran';
|
||||||
static const laporanPenyaluran = '/laporan-penyaluran';
|
static const laporanPenyaluran = '/laporan-penyaluran';
|
||||||
|
216
lib/app/services/jadwal_update_service.dart
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||||
|
|
||||||
|
/// Service untuk menangani pembaruan jadwal real-time dan sinkronisasi antar halaman
|
||||||
|
class JadwalUpdateService extends GetxService {
|
||||||
|
static JadwalUpdateService get to => Get.find<JadwalUpdateService>();
|
||||||
|
|
||||||
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
|
|
||||||
|
// Stream controller untuk mengirim notifikasi pembaruan jadwal ke seluruh aplikasi
|
||||||
|
final _jadwalUpdateStream =
|
||||||
|
StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
Stream<Map<String, dynamic>> get jadwalUpdateStream =>
|
||||||
|
_jadwalUpdateStream.stream;
|
||||||
|
|
||||||
|
// Digunakan untuk menyimpan status terakhir pembaruan jadwal
|
||||||
|
final RxMap<String, DateTime> lastUpdateTimestamp = <String, DateTime>{}.obs;
|
||||||
|
|
||||||
|
// Map untuk melacak jadwal yang sedang dalam pengawasan intensif
|
||||||
|
final RxMap<String, DateTime> _watchedJadwal = <String, DateTime>{}.obs;
|
||||||
|
|
||||||
|
// Timer untuk memeriksa jadwal yang sedang dalam pengawasan
|
||||||
|
Timer? _watchTimer;
|
||||||
|
|
||||||
|
// Mencatat controller yang berlangganan untuk pembaruan
|
||||||
|
final List<String> _subscribedControllers = [];
|
||||||
|
|
||||||
|
// Channel untuk realtime subscription
|
||||||
|
RealtimeChannel? _channel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
_setupRealtimeSubscription();
|
||||||
|
_startWatchTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_jadwalUpdateStream.close();
|
||||||
|
_channel?.unsubscribe();
|
||||||
|
_watchTimer?.cancel();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memulai timer untuk jadwal pengawasan
|
||||||
|
void _startWatchTimer() {
|
||||||
|
_watchTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||||
|
_checkWatchedJadwal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memeriksa jadwal yang sedang diawasi
|
||||||
|
void _checkWatchedJadwal() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final List<String> jadwalToUpdate = [];
|
||||||
|
final List<String> expiredWatches = [];
|
||||||
|
|
||||||
|
_watchedJadwal.forEach((jadwalId, targetTime) {
|
||||||
|
// Jika sudah mencapai atau melewati waktu target
|
||||||
|
if (now.isAtSameMomentAs(targetTime) || now.isAfter(targetTime)) {
|
||||||
|
jadwalToUpdate.add(jadwalId);
|
||||||
|
// Hentikan pengawasan karena sudah waktunya
|
||||||
|
expiredWatches.add(jadwalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sudah lebih dari 5 menit dari waktu target, hentikan pengawasan
|
||||||
|
if (now.difference(targetTime).inMinutes > 5) {
|
||||||
|
expiredWatches.add(jadwalId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hapus jadwal yang sudah tidak perlu diawasi
|
||||||
|
for (var jadwalId in expiredWatches) {
|
||||||
|
_watchedJadwal.remove(jadwalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika ada jadwal yang perlu diperbarui, kirim sinyal untuk memperbarui
|
||||||
|
if (jadwalToUpdate.isNotEmpty) {
|
||||||
|
print('Watched jadwal time reached: ${jadwalToUpdate.join(", ")}');
|
||||||
|
notifyJadwalNeedsCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup langganan ke pembaruan real-time dari Supabase
|
||||||
|
void _setupRealtimeSubscription() {
|
||||||
|
try {
|
||||||
|
// Langganan pembaruan tabel penyaluran_bantuan
|
||||||
|
_channel = _supabaseService.client
|
||||||
|
.channel('penyaluran_bantuan_updates')
|
||||||
|
.onPostgresChanges(
|
||||||
|
event: PostgresChangeEvent.update,
|
||||||
|
schema: 'public',
|
||||||
|
table: 'penyaluran_bantuan',
|
||||||
|
callback: (payload) {
|
||||||
|
if (payload.newRecord != null) {
|
||||||
|
// Dapatkan data jadwal yang diperbarui
|
||||||
|
final jadwalId = payload.newRecord['id'];
|
||||||
|
final newStatus = payload.newRecord['status'];
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Received realtime update for jadwal ID: $jadwalId with status: $newStatus');
|
||||||
|
|
||||||
|
// Kirim notifikasi ke seluruh aplikasi
|
||||||
|
_broadcastUpdate({
|
||||||
|
'type': 'status_update',
|
||||||
|
'jadwal_id': jadwalId,
|
||||||
|
'new_status': newStatus,
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update timestamp
|
||||||
|
lastUpdateTimestamp[jadwalId] = DateTime.now();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mulai berlangganan
|
||||||
|
_channel?.subscribe();
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Realtime subscription for penyaluran_bantuan_updates started successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error setting up realtime subscription: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengirim pembaruan ke semua controller yang berlangganan
|
||||||
|
void _broadcastUpdate(Map<String, dynamic> updateData) {
|
||||||
|
_jadwalUpdateStream.add(updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller dapat mendaftar untuk menerima pembaruan jadwal
|
||||||
|
void registerForUpdates(String controllerId) {
|
||||||
|
if (!_subscribedControllers.contains(controllerId)) {
|
||||||
|
_subscribedControllers.add(controllerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller berhenti menerima pembaruan
|
||||||
|
void unregisterFromUpdates(String controllerId) {
|
||||||
|
_subscribedControllers.remove(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menambahkan jadwal ke pengawasan intensif
|
||||||
|
void addJadwalToWatch(String jadwalId, DateTime targetTime) {
|
||||||
|
print('Adding jadwal $jadwalId to intensive watch for time $targetTime');
|
||||||
|
_watchedJadwal[jadwalId] = targetTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memicu pemeriksaan jadwal segera
|
||||||
|
void notifyJadwalNeedsCheck() {
|
||||||
|
try {
|
||||||
|
// Kirim notifikasi untuk memeriksa jadwal
|
||||||
|
_broadcastUpdate({
|
||||||
|
'type': 'check_required',
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error notifying jadwal check: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muat ulang data jadwal di semua controller yang terdaftar
|
||||||
|
Future<void> notifyJadwalUpdate() async {
|
||||||
|
try {
|
||||||
|
// Kirim notifikasi untuk memuat ulang data
|
||||||
|
_broadcastUpdate({
|
||||||
|
'type': 'reload_required',
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perbarui counter juga saat jadwal diperbarui
|
||||||
|
refreshCounters();
|
||||||
|
|
||||||
|
// Tampilkan notifikasi jika user sedang melihat aplikasi
|
||||||
|
if (Get.isDialogOpen != true && Get.context != null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Jadwal Diperbarui',
|
||||||
|
'Data jadwal telah diperbarui',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.blue.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error notifying jadwal update: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk menyegarkan semua counter terkait penyaluran
|
||||||
|
Future<void> refreshCounters() async {
|
||||||
|
try {
|
||||||
|
// Perbarui counter jika CounterService telah terinisialisasi
|
||||||
|
if (Get.isRegistered<CounterService>()) {
|
||||||
|
final counterService = Get.find<CounterService>();
|
||||||
|
|
||||||
|
// Ambil data jumlah jadwal aktif
|
||||||
|
final jadwalAktifData = await _supabaseService.getJadwalAktif();
|
||||||
|
if (jadwalAktifData != null) {
|
||||||
|
counterService.updateJadwalCounter(jadwalAktifData.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
print('Counters refreshed via JadwalUpdateService');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error refreshing counters: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
204
lib/app/services/notification_service.dart
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
|
|
||||||
|
class NotificationService extends GetxService {
|
||||||
|
static NotificationService get to => Get.find<NotificationService>();
|
||||||
|
|
||||||
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
|
|
||||||
|
// Daftar notifikasi yang belum dibaca
|
||||||
|
final RxList<Map<String, dynamic>> unreadNotifications =
|
||||||
|
<Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
|
// Mengontrol status loading
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
|
// Jumlah notifikasi yang belum dibaca
|
||||||
|
final RxInt unreadCount = 0.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengambil notifikasi dari database
|
||||||
|
Future<void> fetchNotifications() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Ambil notifikasi dari tabel notifikasi di database
|
||||||
|
final userId = _supabaseService.currentUser?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
final response = await _supabaseService.client
|
||||||
|
.from('notifikasi_jadwal')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.eq('is_read', false)
|
||||||
|
.order('created_at', ascending: false)
|
||||||
|
.limit(20);
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
unreadNotifications.value = response;
|
||||||
|
unreadCount.value = unreadNotifications.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching notifications: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengirim notifikasi untuk perubahan status jadwal
|
||||||
|
Future<void> sendJadwalStatusNotification({
|
||||||
|
required String jadwalId,
|
||||||
|
required String newStatus,
|
||||||
|
required String jadwalNama,
|
||||||
|
List<String>? targetUserIds,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final currentUserId = _supabaseService.currentUser?.id;
|
||||||
|
if (currentUserId == null) return;
|
||||||
|
|
||||||
|
// Buat pesan berdasarkan status
|
||||||
|
String message;
|
||||||
|
String title;
|
||||||
|
|
||||||
|
switch (newStatus) {
|
||||||
|
case 'AKTIF':
|
||||||
|
title = 'Jadwal Aktif';
|
||||||
|
message =
|
||||||
|
'Jadwal "$jadwalNama" sekarang aktif dan siap dilaksanakan.';
|
||||||
|
break;
|
||||||
|
case 'TERLAKSANA':
|
||||||
|
title = 'Jadwal Selesai';
|
||||||
|
message = 'Jadwal "$jadwalNama" telah berhasil dilaksanakan.';
|
||||||
|
break;
|
||||||
|
case 'BATALTERLAKSANA':
|
||||||
|
title = 'Jadwal Terlewat';
|
||||||
|
message =
|
||||||
|
'Jadwal "$jadwalNama" telah terlewat dan dibatalkan secara otomatis.';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = 'Perubahan Status Jadwal';
|
||||||
|
message = 'Status jadwal "$jadwalNama" berubah menjadi $newStatus.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada targetUserIds, notifikasi hanya untuk diri sendiri
|
||||||
|
final users = targetUserIds ?? [currentUserId];
|
||||||
|
|
||||||
|
// Simpan notifikasi ke database untuk setiap user
|
||||||
|
for (final userId in users) {
|
||||||
|
await _supabaseService.client.from('notifikasi_jadwal').insert({
|
||||||
|
'user_id': userId,
|
||||||
|
'title': title,
|
||||||
|
'message': message,
|
||||||
|
'jadwal_id': jadwalId,
|
||||||
|
'status': newStatus,
|
||||||
|
'is_read': false,
|
||||||
|
'created_at': DateTime.now().toUtc().toIso8601String(),
|
||||||
|
'created_by': currentUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika perubahan status dari pengguna saat ini, tampilkan notifikasi
|
||||||
|
if (users.contains(currentUserId)) {
|
||||||
|
showStatusChangeNotification(title, message, newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perbarui daftar notifikasi
|
||||||
|
await fetchNotifications();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error sending notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menampilkan notifikasi status di UI
|
||||||
|
void showStatusChangeNotification(
|
||||||
|
String title, String message, String status) {
|
||||||
|
Color backgroundColor;
|
||||||
|
|
||||||
|
// Pilih warna berdasarkan status
|
||||||
|
switch (status) {
|
||||||
|
case 'AKTIF':
|
||||||
|
backgroundColor = Colors.green;
|
||||||
|
break;
|
||||||
|
case 'TERLAKSANA':
|
||||||
|
backgroundColor = Colors.blue;
|
||||||
|
break;
|
||||||
|
case 'BATALTERLAKSANA':
|
||||||
|
backgroundColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
backgroundColor = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan notifikasi
|
||||||
|
Get.snackbar(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: backgroundColor.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
borderRadius: 8,
|
||||||
|
icon: Icon(
|
||||||
|
_getIconForStatus(status),
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menandai notifikasi sebagai telah dibaca
|
||||||
|
Future<void> markAsRead(String notificationId) async {
|
||||||
|
try {
|
||||||
|
await _supabaseService.client
|
||||||
|
.from('notifikasi_jadwal')
|
||||||
|
.update({'is_read': true}).eq('id', notificationId);
|
||||||
|
|
||||||
|
// Hapus dari daftar yang belum dibaca
|
||||||
|
unreadNotifications
|
||||||
|
.removeWhere((notification) => notification['id'] == notificationId);
|
||||||
|
unreadCount.value = unreadNotifications.length;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error marking notification as read: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menandai semua notifikasi sebagai telah dibaca
|
||||||
|
Future<void> markAllAsRead() async {
|
||||||
|
try {
|
||||||
|
final userId = _supabaseService.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
await _supabaseService.client
|
||||||
|
.from('notifikasi_jadwal')
|
||||||
|
.update({'is_read': true})
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.eq('is_read', false);
|
||||||
|
|
||||||
|
// Kosongkan daftar yang belum dibaca
|
||||||
|
unreadNotifications.clear();
|
||||||
|
unreadCount.value = 0;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error marking all notifications as read: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mendapatkan ikon berdasarkan status
|
||||||
|
IconData _getIconForStatus(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'AKTIF':
|
||||||
|
return Icons.event_available;
|
||||||
|
case 'TERLAKSANA':
|
||||||
|
return Icons.check_circle;
|
||||||
|
case 'BATALTERLAKSANA':
|
||||||
|
return Icons.event_busy;
|
||||||
|
default:
|
||||||
|
return Icons.notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -562,19 +562,15 @@ class SupabaseService extends GetxService {
|
|||||||
try {
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
final tomorrow = today.add(const Duration(days: 1));
|
|
||||||
final week = today.add(const Duration(days: 7));
|
final week = today.add(const Duration(days: 7));
|
||||||
|
|
||||||
// Konversi ke UTC untuk query ke database
|
|
||||||
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
|
|
||||||
final weekUtc = week.toUtc().toIso8601String();
|
|
||||||
|
|
||||||
final response = await client
|
final response = await client
|
||||||
.from('penyaluran_bantuan')
|
.from('penyaluran_bantuan')
|
||||||
.select('*')
|
.select('*')
|
||||||
.gte('tanggal_penyaluran', tomorrowUtc)
|
.gte('tanggal_penyaluran', today)
|
||||||
.lt('tanggal_penyaluran', weekUtc)
|
.lt('tanggal_penyaluran', week)
|
||||||
.inFilter('status', ['DIJADWALKAN']);
|
.inFilter('status', ['DIJADWALKAN']).order('tanggal_penyaluran',
|
||||||
|
ascending: true);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -651,15 +647,128 @@ class SupabaseService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metode untuk memperbarui status jadwal
|
// Metode untuk memperbarui status jadwal
|
||||||
Future<void> updateJadwalStatus(String jadwalId, String status) async {
|
Future<void> updateJadwalStatus(String jadwalId, String newStatus) async {
|
||||||
try {
|
try {
|
||||||
await client.from('penyaluran_bantuan').update({
|
await client.from('penyaluran_bantuan').update({
|
||||||
'status': status,
|
'status': newStatus,
|
||||||
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
'updated_at': DateTime.now().toUtc().toIso8601String()
|
||||||
}).eq('id', jadwalId);
|
}).eq('id', jadwalId);
|
||||||
|
|
||||||
|
print('Jadwal status updated: $jadwalId -> $newStatus');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error updating jadwal status: $e');
|
print('Error updating jadwal status: $e');
|
||||||
throw e.toString();
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status jadwal penyaluran secara batch untuk efisiensi
|
||||||
|
Future<void> batchUpdateJadwalStatus(
|
||||||
|
Map<String, String> jadwalUpdates) async {
|
||||||
|
if (jadwalUpdates.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
print('Attempting batch update for ${jadwalUpdates.length} jadwal');
|
||||||
|
final timestamp = DateTime.now().toUtc().toIso8601String();
|
||||||
|
|
||||||
|
// Format data sesuai dengan yang diharapkan oleh SQL function
|
||||||
|
final List<Map<String, dynamic>> formattedUpdates = jadwalUpdates.entries
|
||||||
|
.map((e) => {'id': e.key, 'status': e.value})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
print('Formatted updates: $formattedUpdates');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Coba gunakan RPC dulu - kirim sebagai array dari objek JSON
|
||||||
|
final result = await client.rpc('batch_update_jadwal_status', params: {
|
||||||
|
'jadwal_updates': formattedUpdates,
|
||||||
|
'updated_timestamp': timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Batch update via RPC response: $result');
|
||||||
|
|
||||||
|
// Periksa hasil untuk mengkonfirmasi berapa banyak yang berhasil diupdate
|
||||||
|
if (result != null) {
|
||||||
|
final bool success = result['success'] == true;
|
||||||
|
final int updatedCount = result['updated_count'] ?? 0;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
print('Successfully updated $updatedCount records via RPC');
|
||||||
|
|
||||||
|
// Log ID yang berhasil diupdate
|
||||||
|
final List<String> successIds =
|
||||||
|
List<String>.from(result['success_ids'] ?? []);
|
||||||
|
if (successIds.isNotEmpty) {
|
||||||
|
print(
|
||||||
|
'Successfully updated jadwal IDs: ${successIds.join(", ")}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika ada yang gagal, log untuk debugging
|
||||||
|
if (updatedCount < jadwalUpdates.length) {
|
||||||
|
print(
|
||||||
|
'Warning: ${jadwalUpdates.length - updatedCount} records failed to update');
|
||||||
|
|
||||||
|
// Periksa apakah ada informasi error
|
||||||
|
if (result['errors'] != null) {
|
||||||
|
final int errorCount = result['errors']['count'] ?? 0;
|
||||||
|
if (errorCount > 0) {
|
||||||
|
final List<String> errorIds =
|
||||||
|
List<String>.from(result['errors']['ids'] ?? []);
|
||||||
|
final List<String> errorMessages =
|
||||||
|
List<String>.from(result['errors']['messages'] ?? []);
|
||||||
|
|
||||||
|
for (int i = 0; i < errorCount; i++) {
|
||||||
|
if (i < errorIds.length && i < errorMessages.length) {
|
||||||
|
print(
|
||||||
|
'Error updating jadwal ${errorIds[i]}: ${errorMessages[i]}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update individual yang gagal menggunakan metode satu per satu
|
||||||
|
for (var entry in jadwalUpdates.entries) {
|
||||||
|
if (!successIds.contains(entry.key)) {
|
||||||
|
try {
|
||||||
|
await updateJadwalStatus(entry.key, entry.value);
|
||||||
|
print('Fallback update successful for jadwal ${entry.key}');
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'Fallback update also failed for jadwal ${entry.key}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'Batch update reported failure. Falling back to individual updates.');
|
||||||
|
_fallbackToIndividualUpdates(jadwalUpdates);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'Batch update returned null result. Falling back to individual updates.');
|
||||||
|
_fallbackToIndividualUpdates(jadwalUpdates);
|
||||||
|
}
|
||||||
|
} catch (rpcError) {
|
||||||
|
print('RPC batch update failed: $rpcError');
|
||||||
|
print('Falling back to individual updates');
|
||||||
|
_fallbackToIndividualUpdates(jadwalUpdates);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error in batch update process: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function untuk fallback ke individual updates
|
||||||
|
Future<void> _fallbackToIndividualUpdates(
|
||||||
|
Map<String, String> jadwalUpdates) async {
|
||||||
|
for (var entry in jadwalUpdates.entries) {
|
||||||
|
try {
|
||||||
|
await updateJadwalStatus(entry.key, entry.value);
|
||||||
|
print('Individual update successful: ${entry.key} -> ${entry.value}');
|
||||||
|
} catch (updateError) {
|
||||||
|
print('Failed to update jadwal ${entry.key}: $updateError');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -874,7 +983,7 @@ class SupabaseService extends GetxService {
|
|||||||
.select('stok_bantuan_id, jumlah')
|
.select('stok_bantuan_id, jumlah')
|
||||||
.eq('id', penitipanId);
|
.eq('id', penitipanId);
|
||||||
|
|
||||||
if (response == null || response.isEmpty) {
|
if (response.isEmpty) {
|
||||||
throw 'Data penitipan tidak ditemukan';
|
throw 'Data penitipan tidak ditemukan';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1930,8 +2039,8 @@ class SupabaseService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (jenisPerubahan != null) {
|
if (jenisPerubahan != null) {
|
||||||
filterString += (filterString.isNotEmpty ? ',' : '') +
|
filterString +=
|
||||||
'jenis_perubahan.eq.$jenisPerubahan';
|
'${filterString.isNotEmpty ? ',' : ''}jenis_perubahan.eq.$jenisPerubahan';
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await client.from('riwayat_stok').select('''
|
final response = await client.from('riwayat_stok').select('''
|
||||||
@ -2006,7 +2115,7 @@ class SupabaseService extends GetxService {
|
|||||||
print('Stok berhasil ditambahkan dari penitipan');
|
print('Stok berhasil ditambahkan dari penitipan');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error adding stok from penitipan: $e');
|
print('Error adding stok from penitipan: $e');
|
||||||
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2058,7 +2167,7 @@ class SupabaseService extends GetxService {
|
|||||||
print('Stok berhasil dikurangi dari penyaluran');
|
print('Stok berhasil dikurangi dari penyaluran');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error reducing stok from penyaluran: $e');
|
print('Error reducing stok from penyaluran: $e');
|
||||||
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2075,7 +2184,7 @@ class SupabaseService extends GetxService {
|
|||||||
String fotoBuktiUrl = '';
|
String fotoBuktiUrl = '';
|
||||||
if (fotoBuktiPath.isNotEmpty) {
|
if (fotoBuktiPath.isNotEmpty) {
|
||||||
final String fileName =
|
final String fileName =
|
||||||
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
|
||||||
final fileResponse = await client.storage.from('stok_bukti').upload(
|
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||||
fileName,
|
fileName,
|
||||||
File(fotoBuktiPath),
|
File(fotoBuktiPath),
|
||||||
@ -2125,7 +2234,7 @@ class SupabaseService extends GetxService {
|
|||||||
print('Stok berhasil ditambahkan secara manual');
|
print('Stok berhasil ditambahkan secara manual');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error adding stok manually: $e');
|
print('Error adding stok manually: $e');
|
||||||
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2164,7 +2273,7 @@ class SupabaseService extends GetxService {
|
|||||||
String fotoBuktiUrl = '';
|
String fotoBuktiUrl = '';
|
||||||
if (fotoBuktiPath.isNotEmpty) {
|
if (fotoBuktiPath.isNotEmpty) {
|
||||||
final String fileName =
|
final String fileName =
|
||||||
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
|
||||||
final fileResponse = await client.storage.from('stok_bukti').upload(
|
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||||
fileName,
|
fileName,
|
||||||
File(fotoBuktiPath),
|
File(fotoBuktiPath),
|
||||||
@ -2198,7 +2307,7 @@ class SupabaseService extends GetxService {
|
|||||||
print('Stok berhasil dikurangi secara manual');
|
print('Stok berhasil dikurangi secara manual');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error reducing stok manually: $e');
|
print('Error reducing stok manually: $e');
|
||||||
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
/// Kelas pembantu untuk manipulasi tanggal dan waktu
|
|
||||||
class DateHelper {
|
|
||||||
/// Format tanggal ke format Indonesia (dd MMM yyyy)
|
|
||||||
static String formatDate(
|
|
||||||
DateTime? dateTime, {
|
|
||||||
String format = 'dd MMM yyyy',
|
|
||||||
String locale = 'id_ID',
|
|
||||||
String defaultValue = 'Belum ditentukan',
|
|
||||||
}) {
|
|
||||||
if (dateTime == null) return defaultValue;
|
|
||||||
try {
|
|
||||||
return DateFormat(format, locale).format(dateTime.toLocal());
|
|
||||||
} catch (e) {
|
|
||||||
return dateTime.toString().split(' ')[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format nilai ke dalam format mata uang Rupiah
|
|
||||||
static String formatRupiah(
|
|
||||||
num? value, {
|
|
||||||
String symbol = 'Rp',
|
|
||||||
int decimalDigits = 0,
|
|
||||||
String defaultValue = 'Rp 0',
|
|
||||||
}) {
|
|
||||||
if (value == null) return defaultValue;
|
|
||||||
try {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id_ID',
|
|
||||||
symbol: '$symbol ',
|
|
||||||
decimalDigits: decimalDigits,
|
|
||||||
);
|
|
||||||
return formatter.format(value);
|
|
||||||
} catch (e) {
|
|
||||||
// Format manual
|
|
||||||
return '$symbol ${value.toStringAsFixed(decimalDigits).replaceAllMapped(
|
|
||||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
|
||||||
(Match m) => '${m[1]}.',
|
|
||||||
)}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import 'package:intl/intl.dart';
|
|||||||
///
|
///
|
||||||
/// Kelas ini berisi fungsi-fungsi untuk memformat dan memanipulasi
|
/// Kelas ini berisi fungsi-fungsi untuk memformat dan memanipulasi
|
||||||
/// tanggal dan waktu.
|
/// tanggal dan waktu.
|
||||||
class DateTimeHelper {
|
class FormatHelper {
|
||||||
/// Mengkonversi DateTime dari UTC ke timezone lokal
|
/// Mengkonversi DateTime dari UTC ke timezone lokal
|
||||||
static DateTime toLocalDateTime(DateTime utcDateTime) {
|
static DateTime toLocalDateTime(DateTime utcDateTime) {
|
||||||
return utcDateTime.toLocal();
|
return utcDateTime.toLocal();
|
||||||
@ -70,7 +70,6 @@ class DateTimeHelper {
|
|||||||
static String formatDateTime(
|
static String formatDateTime(
|
||||||
DateTime? dateTime, {
|
DateTime? dateTime, {
|
||||||
String format = 'dd MMM yyyy HH:mm',
|
String format = 'dd MMM yyyy HH:mm',
|
||||||
String locale = 'id_ID',
|
|
||||||
String defaultValue = 'Belum ditentukan',
|
String defaultValue = 'Belum ditentukan',
|
||||||
}) {
|
}) {
|
||||||
if (dateTime == null) return defaultValue;
|
if (dateTime == null) return defaultValue;
|
||||||
@ -78,7 +77,7 @@ class DateTimeHelper {
|
|||||||
// Pastikan tanggal dan waktu dalam timezone lokal
|
// Pastikan tanggal dan waktu dalam timezone lokal
|
||||||
final localDateTime = toLocalDateTime(dateTime);
|
final localDateTime = toLocalDateTime(dateTime);
|
||||||
try {
|
try {
|
||||||
return DateFormat(format, locale).format(localDateTime);
|
return DateFormat(format).format(localDateTime);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error formatting date time: $e');
|
print('Error formatting date time: $e');
|
||||||
return localDateTime.toString(); // Fallback to basic format
|
return localDateTime.toString(); // Fallback to basic format
|
||||||
@ -197,8 +196,10 @@ class DateTimeHelper {
|
|||||||
final String tanggal = localDateTime.day.toString().padLeft(2, '0');
|
final String tanggal = localDateTime.day.toString().padLeft(2, '0');
|
||||||
final String bulan = namaBulan[localDateTime.month - 1];
|
final String bulan = namaBulan[localDateTime.month - 1];
|
||||||
final String tahun = localDateTime.year.toString();
|
final String tahun = localDateTime.year.toString();
|
||||||
|
final String jam = localDateTime.hour.toString().padLeft(2, '0');
|
||||||
|
final String menit = localDateTime.minute.toString().padLeft(2, '0');
|
||||||
|
|
||||||
return '$hari, $tanggal $bulan $tahun';
|
return '$hari, $tanggal $bulan $tahun $jam:$menit';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format angka dengan pemisah ribuan
|
/// Format angka dengan pemisah ribuan
|
||||||
|
@ -38,14 +38,20 @@ class AppDrawer extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 30,
|
radius: 30,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||||
backgroundImage:
|
backgroundImage: avatar != null && avatar!.isNotEmpty
|
||||||
avatar != null ? NetworkImage(avatar!) : null,
|
? NetworkImage(avatar!)
|
||||||
child: avatar == null
|
: null,
|
||||||
? const Icon(
|
child: (avatar == null || avatar!.isEmpty)
|
||||||
Icons.person,
|
? Text(
|
||||||
size: 40,
|
nama.isNotEmpty
|
||||||
color: AppTheme.primaryColor,
|
? nama.substring(0, 1).toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||||
|
|
||||||
class BantuanCard extends StatelessWidget {
|
class BantuanCard extends StatelessWidget {
|
||||||
@ -17,17 +17,11 @@ class BantuanCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currencyFormat = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
||||||
String formattedJumlah = '';
|
String formattedJumlah = '';
|
||||||
if (item.jumlahBantuan != null) {
|
if (item.jumlahBantuan != null) {
|
||||||
if (item.isUang == true) {
|
if (item.isUang == true) {
|
||||||
formattedJumlah = currencyFormat.format(item.jumlahBantuan);
|
formattedJumlah = FormatHelper.formatRupiah(item.jumlahBantuan);
|
||||||
} else {
|
} else {
|
||||||
formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}';
|
formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}';
|
||||||
}
|
}
|
||||||
@ -120,8 +114,8 @@ class BantuanCard extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.tanggalPenerimaan != null
|
item.tanggalPenerimaan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(
|
||||||
.format(item.tanggalPenerimaan!)
|
item.tanggalPenerimaan!)
|
||||||
: '-',
|
: '-',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey.shade600,
|
color: Colors.grey.shade600,
|
||||||
@ -373,8 +367,8 @@ class BantuanCard extends StatelessWidget {
|
|||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
'Tanggal:',
|
'Tanggal:',
|
||||||
item.tanggalPenerimaan != null
|
item.tanggalPenerimaan != null
|
||||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
? FormatHelper.formatDateTime(
|
||||||
.format(item.tanggalPenerimaan!)
|
item.tanggalPenerimaan!)
|
||||||
: '-',
|
: '-',
|
||||||
),
|
),
|
||||||
const Divider(height: 16),
|
const Divider(height: 16),
|
||||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_colors.dart';
|
import 'package:penyaluran_app/app/theme/app_colors.dart';
|
||||||
|
import 'package:penyaluran_app/app/widgets/dialogs/show_image_dialog.dart';
|
||||||
|
|
||||||
/// Dialog untuk menampilkan detail penitipan bantuan
|
/// Dialog untuk menampilkan detail penitipan bantuan
|
||||||
///
|
///
|
||||||
@ -48,7 +49,7 @@ class DetailPenitipanDialog {
|
|||||||
),
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Penitipan',
|
'Tanggal Penitipan',
|
||||||
DateTimeHelper.formatDateTime(
|
FormatHelper.formatDateTime(
|
||||||
item.tanggalPenitipan ?? item.createdAt),
|
item.tanggalPenitipan ?? item.createdAt),
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
@ -63,7 +64,7 @@ class DetailPenitipanDialog {
|
|||||||
if (item.tanggalVerifikasi != null)
|
if (item.tanggalVerifikasi != null)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Tanggal Verifikasi',
|
'Tanggal Verifikasi',
|
||||||
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
|
FormatHelper.formatDateTime(item.tanggalVerifikasi),
|
||||||
),
|
),
|
||||||
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
|
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
|
||||||
_buildInfoRow('Deskripsi', item.deskripsi!),
|
_buildInfoRow('Deskripsi', item.deskripsi!),
|
||||||
@ -143,50 +144,7 @@ class DetailPenitipanDialog {
|
|||||||
|
|
||||||
/// Menampilkan gambar dalam layar penuh
|
/// Menampilkan gambar dalam layar penuh
|
||||||
static void showFullScreenImage(BuildContext context, String imageUrl) {
|
static void showFullScreenImage(BuildContext context, String imageUrl) {
|
||||||
Navigator.of(context).push(
|
ShowImageDialog.showFullScreen(context, imageUrl);
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
iconTheme: const IconThemeData(color: Colors.white),
|
|
||||||
),
|
|
||||||
body: Container(
|
|
||||||
color: Colors.black,
|
|
||||||
child: Center(
|
|
||||||
child: InteractiveViewer(
|
|
||||||
panEnabled: true,
|
|
||||||
boundaryMargin: const EdgeInsets.all(20),
|
|
||||||
minScale: 0.5,
|
|
||||||
maxScale: 4,
|
|
||||||
child: Image.network(
|
|
||||||
imageUrl,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: loadingProgress.expectedTotalBytes != null
|
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return const Center(
|
|
||||||
child: Text(
|
|
||||||
'Gagal memuat gambar',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Membangun baris informasi
|
/// Membangun baris informasi
|
||||||
|
254
lib/app/widgets/dialogs/show_image_dialog.dart
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
/// Dialog untuk menampilkan gambar dalam ukuran besar
|
||||||
|
///
|
||||||
|
/// Komponen ini dapat digunakan untuk menampilkan gambar dari URL
|
||||||
|
/// dengan kemampuan zoom dan pan pada gambar.
|
||||||
|
class ShowImageDialog {
|
||||||
|
/// Menampilkan dialog gambar
|
||||||
|
///
|
||||||
|
/// [context] adalah BuildContext untuk menampilkan dialog
|
||||||
|
/// [imageUrl] adalah URL gambar yang akan ditampilkan
|
||||||
|
/// [title] adalah judul dari dialog, default 'Bukti Foto'
|
||||||
|
static void show(
|
||||||
|
BuildContext context,
|
||||||
|
String imageUrl, {
|
||||||
|
String title = 'Bukti Foto',
|
||||||
|
}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.download,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () => _downloadImage(context, imageUrl),
|
||||||
|
tooltip: 'Unduh Gambar',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.5,
|
||||||
|
child: InteractiveViewer(
|
||||||
|
panEnabled: true,
|
||||||
|
boundaryMargin: const EdgeInsets.all(16),
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 4,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.red, size: 48),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
'Gagal memuat gambar: $error',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.zoom_in, size: 20, color: Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Cubit untuk memperbesar/memperkecil',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menampilkan dialog gambar layar penuh
|
||||||
|
///
|
||||||
|
/// Versi layar penuh dari dialog gambar
|
||||||
|
/// [context] adalah BuildContext untuk menampilkan dialog
|
||||||
|
/// [imageUrl] adalah URL gambar yang akan ditampilkan
|
||||||
|
static void showFullScreen(BuildContext context, String imageUrl) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: EdgeInsets.zero,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InteractiveViewer(
|
||||||
|
panEnabled: true,
|
||||||
|
boundaryMargin: const EdgeInsets.all(20),
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 4.0,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.white, size: 32),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Gagal memuat gambar',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.download,
|
||||||
|
color: Colors.white, size: 30),
|
||||||
|
onPressed: () => _downloadImage(context, imageUrl),
|
||||||
|
tooltip: 'Unduh Gambar',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close,
|
||||||
|
color: Colors.white, size: 30),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mengunduh gambar dari URL dan menyimpannya ke penyimpanan lokal
|
||||||
|
///
|
||||||
|
/// [context] adalah BuildContext untuk menampilkan snackbar
|
||||||
|
/// [imageUrl] adalah URL gambar yang akan diunduh
|
||||||
|
static Future<void> _downloadImage(
|
||||||
|
BuildContext context, String imageUrl) async {
|
||||||
|
try {
|
||||||
|
// Tampilkan indikator loading
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Mengunduh gambar...'),
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ambil data gambar dari URL
|
||||||
|
final response = await http.get(Uri.parse(imageUrl));
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Gagal mengunduh gambar');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan direktori penyimpanan sementara
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final fileName =
|
||||||
|
'bukti_foto_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
final file = File('${tempDir.path}/$fileName');
|
||||||
|
|
||||||
|
// Tulis data ke file
|
||||||
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
|
|
||||||
|
// Bagikan file
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path)],
|
||||||
|
text: 'Bukti Foto',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tampilkan pesan sukses
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Gambar berhasil diunduh dan siap dibagikan'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Tampilkan pesan error
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Gagal mengunduh gambar: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ export 'cards/info_card.dart';
|
|||||||
// Dialogs
|
// Dialogs
|
||||||
export 'dialogs/detail_penitipan_dialog.dart';
|
export 'dialogs/detail_penitipan_dialog.dart';
|
||||||
export 'dialogs/confirmation_dialog.dart';
|
export 'dialogs/confirmation_dialog.dart';
|
||||||
|
export 'dialogs/show_image_dialog.dart';
|
||||||
|
|
||||||
// Indicators
|
// Indicators
|
||||||
export 'indicators/loading_indicator.dart';
|
export 'indicators/loading_indicator.dart';
|
||||||
|
@ -9,6 +9,7 @@ import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart
|
|||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:syncfusion_localizations/syncfusion_localizations.dart';
|
import 'package:syncfusion_localizations/syncfusion_localizations.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/notification_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -27,11 +28,26 @@ void main() async {
|
|||||||
|
|
||||||
// Inisialisasi service
|
// Inisialisasi service
|
||||||
Future<void> initServices() async {
|
Future<void> initServices() async {
|
||||||
await Get.putAsync(() => SupabaseService().init());
|
print('Initializing services...');
|
||||||
await Get.putAsync(() => AuthService().init());
|
// Inisialisasi SupabaseService dengan pendekatan async
|
||||||
|
final supabaseService =
|
||||||
|
await Get.putAsync(() => SupabaseService().init(), permanent: true);
|
||||||
|
print('SupabaseService initialized: ${supabaseService != null}');
|
||||||
|
|
||||||
|
// Inisialisasi AuthService
|
||||||
|
final authService =
|
||||||
|
await Get.putAsync(() => AuthService().init(), permanent: true);
|
||||||
|
print('AuthService initialized: ${authService != null}');
|
||||||
|
|
||||||
// Inisialisasi AuthController secara global
|
// Inisialisasi AuthController secara global
|
||||||
Get.put(AuthController(), permanent: true);
|
final authController = Get.put(AuthController(), permanent: true);
|
||||||
|
print('AuthController initialized: ${authController != null}');
|
||||||
|
|
||||||
|
// Register NotificationService
|
||||||
|
final notificationService = Get.put(NotificationService(), permanent: true);
|
||||||
|
print('NotificationService initialized: ${notificationService != null}');
|
||||||
|
|
||||||
|
print('All services initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@ -40,7 +56,7 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetMaterialApp(
|
return GetMaterialApp(
|
||||||
title: 'Penerimaan App',
|
title: 'DisalurKita',
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: AppTheme.darkTheme,
|
||||||
themeMode: ThemeMode.light, // Default ke tema terang
|
themeMode: ThemeMode.light, // Default ke tema terang
|
||||||
@ -60,6 +76,7 @@ class MyApp extends StatelessWidget {
|
|||||||
Locale('id', 'ID'), // Indonesia
|
Locale('id', 'ID'), // Indonesia
|
||||||
Locale('en', 'US'), // English
|
Locale('en', 'US'), // English
|
||||||
],
|
],
|
||||||
|
// initialBinding tidak diperlukan lagi karena service sudah diinisialisasi di initServices()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import file_selector_macos
|
|||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import open_file_mac
|
import open_file_mac
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
18
pubspec.lock
@ -670,7 +670,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
version: "0.0.3"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
@ -837,6 +837,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.1.4"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -94,6 +94,8 @@ dependencies:
|
|||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
# Library untuk cached image
|
# Library untuk cached image
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
|
share_plus: ^10.1.4
|
||||||
|
path: ^1.9.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
app_links
|
app_links
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|