h-1 lebaran
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>Õ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt <08>Õ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08>Õ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08>Õ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
n
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08>Õ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08>Õ<EFBFBD><EFBFBD>2y
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
w
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08>Õ<EFBFBD><EFBFBD>2
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
|
||||
x
|
||||
x
|
||||
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 ~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||
u
|
||||
u
|
||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ȕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <08>ȕ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json <08>ȕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <08>ȕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
p
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja <08>ȕ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt <08>ȕ<EFBFBD><EFBFBD>2{
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt <08>ȕ<EFBFBD><EFBFBD>2
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
||||
z
|
||||
z
|
||||
xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 <09>
|
||||
~
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
w
|
||||
w
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>͕<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt <08>͕<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2x
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2x
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json <08>͕<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
|
||||
{
|
||||
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json <08>͕<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
|
||||
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
|
||||
h
|
||||
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja <08>͕<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
|
||||
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
|
||||
l
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08>͕<EFBFBD><EFBFBD>2s
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2s
|
||||
q
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08>͕<EFBFBD><EFBFBD>2
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json <08><><EFBFBD><EFBFBD><EFBFBD>2 x
|
||||
v
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
o
|
||||
o
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\prefab_config.json <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt <08>ϕ<EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt Ұ<EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
k
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja <08>ϕ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja Ұ<EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
o
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt <08>ϕ<EFBFBD><EFBFBD>2v
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt Ұ<EFBFBD><EFBFBD><EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt <08>ϕ<EFBFBD><EFBFBD>2
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt Ұ<EFBFBD><EFBFBD><EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||
u
|
||||
u
|
||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json Ұ<><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
|
||||
<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
|
||||
r
|
||||
r
|
||||
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" />
|
||||
|
||||
<application
|
||||
android:label="penyaluran_app"
|
||||
android:label="DisalurKita"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@ -47,5 +47,24 @@
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</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>
|
||||
</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>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Aplikasi memerlukan akses mikrofon untuk merekam video</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
<string>file</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -12,6 +12,7 @@ class LokasiPenyaluranModel {
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? petugasDesaId; // Referensi ke PetugasDesa
|
||||
final bool isLokasiTitip; // Field baru untuk menentukan lokasi penitipan
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
@ -27,6 +28,7 @@ class LokasiPenyaluranModel {
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.petugasDesaId,
|
||||
this.isLokasiTitip = false, // Nilai default false
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
@ -49,6 +51,7 @@ class LokasiPenyaluranModel {
|
||||
latitude: json["latitude"]?.toDouble(),
|
||||
longitude: json["longitude"]?.toDouble(),
|
||||
petugasDesaId: json["petugas_desa_id"],
|
||||
isLokasiTitip: json["is_lokasi_titip"] ?? false,
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: json["updated_at"] == null
|
||||
? null
|
||||
@ -67,6 +70,7 @@ class LokasiPenyaluranModel {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"petugas_desa_id": petugasDesaId,
|
||||
"is_lokasi_titip": isLokasiTitip,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt?.toIso8601String(),
|
||||
};
|
||||
|
@ -71,6 +71,14 @@ class PenyaluranBantuanModel {
|
||||
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) =>
|
||||
PenyaluranBantuanModel.fromJson(json.decode(str));
|
||||
|
||||
@ -126,4 +134,49 @@ class PenyaluranBantuanModel {
|
||||
"created_at": createdAt?.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 {
|
||||
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 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
|
||||
final RxBool _hasLoadedProfile = false.obs;
|
||||
|
||||
@ -376,6 +380,65 @@ class AuthController extends GetxController {
|
||||
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
|
||||
Future<void> refreshUserData() async {
|
||||
try {
|
||||
@ -543,4 +606,14 @@ class AuthController extends GetxController {
|
||||
noHpController.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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: controller.loginFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
// Logo atau Judul
|
||||
const Center(
|
||||
child: Text(
|
||||
'Penyaluran App',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
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: controller.loginFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/images/logo-disalurkita.png',
|
||||
width: 250,
|
||||
height: 250,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Masuk ke akun Anda',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
|
||||
const Center(
|
||||
child: Text(
|
||||
'Masuk ke akun Anda',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF546E7A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Email Field
|
||||
TextFormField(
|
||||
controller: controller.emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: const Icon(Icons.email),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
// 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: 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
|
||||
TextFormField(
|
||||
controller: controller.passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
// Password Field
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
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
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
// Implementasi lupa password
|
||||
},
|
||||
child: const Text('Lupa Password?'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 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),
|
||||
// Forgot Password
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.toNamed(Routes.forgotPassword),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF1565C0),
|
||||
),
|
||||
child: const Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? const SpinKitThreeBounce(
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
)
|
||||
: const Text(
|
||||
'MASUK',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Login 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
|
||||
: 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(height: 20),
|
||||
|
||||
// Divider
|
||||
const Row(
|
||||
children: [
|
||||
Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child:
|
||||
Text('ATAU', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Register Donatur Button
|
||||
OutlinedButton(
|
||||
onPressed: () => Get.toNamed(Routes.registerDonatur),
|
||||
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(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Informasi Penting',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE65100),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Pendaftaran warga hanya dapat dilakukan melalui aplikasi verifikasi data warga. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF424242),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Daftar Sebagai Donatur'),
|
||||
title: const Text('Daftar Donatur'),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
@ -23,29 +31,60 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
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 Center(
|
||||
child: Text(
|
||||
'Isi data untuk mendaftar sebagai donatur',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
// Header dengan icon dan judul
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
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
|
||||
TextFormField(
|
||||
@ -53,9 +92,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Lengkap',
|
||||
prefixIcon: const Icon(Icons.person),
|
||||
border: OutlineInputBorder(
|
||||
hintText: 'Masukkan nama lengkap Anda',
|
||||
prefixIcon: const Icon(Icons.person, color: Colors.blue),
|
||||
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.validateDonaturNama,
|
||||
@ -68,9 +120,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: const Icon(Icons.email),
|
||||
border: OutlineInputBorder(
|
||||
hintText: 'contoh@email.com',
|
||||
prefixIcon: const Icon(Icons.email, color: Colors.blue),
|
||||
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.validateEmail,
|
||||
@ -78,34 +143,101 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Password
|
||||
TextFormField(
|
||||
controller: controller.passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
validator: controller.validatePassword,
|
||||
),
|
||||
Obx(() => TextFormField(
|
||||
controller: controller.passwordController,
|
||||
obscureText: controller.isPasswordHidden.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Minimal 8 karakter',
|
||||
prefixIcon:
|
||||
const Icon(Icons.lock, color: Colors.blue),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
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),
|
||||
|
||||
// Confirm Password
|
||||
TextFormField(
|
||||
controller: controller.confirmPasswordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Konfirmasi Password',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
Obx(() => TextFormField(
|
||||
controller: controller.confirmPasswordController,
|
||||
obscureText: controller.isConfirmPasswordHidden.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Konfirmasi Password',
|
||||
hintText: 'Masukkan password yang sama',
|
||||
prefixIcon: const Icon(Icons.lock_outline,
|
||||
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 Divider(),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// No HP
|
||||
TextFormField(
|
||||
@ -113,9 +245,22 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nomor HP',
|
||||
prefixIcon: const Icon(Icons.phone),
|
||||
border: OutlineInputBorder(
|
||||
hintText: 'Masukkan nomor HP aktif',
|
||||
prefixIcon: const Icon(Icons.phone, color: Colors.blue),
|
||||
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.validateDonaturNoHp,
|
||||
@ -128,10 +273,23 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
keyboardType: TextInputType.streetAddress,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
prefixIcon: const Icon(Icons.home),
|
||||
border: OutlineInputBorder(
|
||||
labelText: 'Alamat Lengkap',
|
||||
hintText: 'Masukkan alamat lengkap Anda',
|
||||
prefixIcon: const Icon(Icons.home, color: Colors.blue),
|
||||
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.validateDonaturAlamat,
|
||||
@ -139,70 +297,160 @@ class RegisterDonaturView extends GetView<AuthController> {
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Jenis Donatur (Dropdown)
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.jenisController.text.isEmpty
|
||||
? 'Individu'
|
||||
: controller.jenisController.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jenis Donatur',
|
||||
prefixIcon: const Icon(Icons.category),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: controller.jenisController.text.isEmpty
|
||||
? '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
|
||||
Obx(() => ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: controller.registerDonatur,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? const SpinKitThreeBounce(
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
)
|
||||
: const Text(
|
||||
'DAFTAR',
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Catatan Informasi
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.blue),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
'Informasi',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
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),
|
||||
|
||||
// Login Link
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Sudah punya akun?'),
|
||||
TextButton(
|
||||
onPressed: () => Get.offAllNamed(Routes.login),
|
||||
child: const Text('Masuk'),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
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/user_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/services/supabase_service.dart';
|
||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||
@ -45,6 +46,10 @@ class DonaturDashboardController extends GetxController {
|
||||
// Data untuk stok bantuan yang tersedia
|
||||
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
|
||||
|
||||
// Data untuk lokasi penyaluran
|
||||
final RxList<LokasiPenyaluranModel> lokasiPenyaluran =
|
||||
<LokasiPenyaluranModel>[].obs;
|
||||
|
||||
// Indikator loading
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
@ -199,6 +204,9 @@ class DonaturDashboardController extends GetxController {
|
||||
// Ambil data stok bantuan
|
||||
await fetchStokBantuan();
|
||||
|
||||
// Ambil data lokasi penyaluran
|
||||
await fetchLokasiPenyaluran();
|
||||
|
||||
// Ambil data notifikasi
|
||||
await fetchNotifikasi();
|
||||
} catch (e) {
|
||||
@ -233,7 +241,7 @@ class DonaturDashboardController extends GetxController {
|
||||
.from('penyaluran_bantuan')
|
||||
.select(
|
||||
'*, 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
|
||||
final allJadwal = response
|
||||
@ -243,9 +251,7 @@ class DonaturDashboardController extends GetxController {
|
||||
|
||||
// Filter jadwal yang tanggalnya lebih besar dari hari ini
|
||||
jadwalPenyaluran.value = allJadwal
|
||||
.where((jadwal) =>
|
||||
jadwal.tanggalPenyaluran != null &&
|
||||
jadwal.tanggalPenyaluran!.isAfter(now))
|
||||
.where((jadwal) => jadwal.tanggalPenyaluran != null)
|
||||
.toList();
|
||||
} catch (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
|
||||
Future<void> fetchNotifikasi() async {
|
||||
try {
|
||||
@ -386,6 +409,7 @@ class DonaturDashboardController extends GetxController {
|
||||
double jumlah,
|
||||
String deskripsi,
|
||||
String? skemaBantuanId,
|
||||
String? lokasiPenyaluranId,
|
||||
) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
@ -426,15 +450,25 @@ class DonaturDashboardController extends GetxController {
|
||||
'tanggal_penitipan': DateTime.now().toIso8601String(),
|
||||
'foto_bantuan': fotoBantuanUrls,
|
||||
'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
|
||||
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
|
||||
resetFotoBantuan();
|
||||
@ -442,19 +476,13 @@ class DonaturDashboardController extends GetxController {
|
||||
// Ambil data penitipan bantuan yang baru
|
||||
await fetchPenitipanBantuan();
|
||||
|
||||
// Tampilkan pesan sukses
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
// Kembali ke halaman utama
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
print('Error creating penitipan bantuan: $e');
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat mengirim penitipan bantuan: $e',
|
||||
'Terjadi kesalahan: $e',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
@ -475,7 +503,7 @@ class DonaturDashboardController extends GetxController {
|
||||
.eq('id', lokasiId)
|
||||
.single();
|
||||
|
||||
if (response != null && response['nama'] != null) {
|
||||
if (response['nama'] != null) {
|
||||
return response['nama'] as String;
|
||||
}
|
||||
return null;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
@ -36,13 +36,57 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatisticSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildUpcomingEvents(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRecentPenitipan(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -101,14 +145,24 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
backgroundImage: controller.profilePhotoUrl != null
|
||||
backgroundImage: controller.profilePhotoUrl != null &&
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
child: (controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty)
|
||||
? Text(
|
||||
controller.nama.isNotEmpty
|
||||
? controller.nama
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -263,7 +317,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
child: _buildStatCard(
|
||||
title: 'Diterima',
|
||||
value:
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}',
|
||||
'${controller.penitipanBantuan.where((p) => p.status == 'TERVERIFIKASI').length}',
|
||||
icon: Icons.check_circle_outline,
|
||||
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({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
@ -545,7 +480,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
|
||||
Widget _buildEventCard(dynamic event) {
|
||||
final formattedDate = event.tanggalPenyaluran != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDateTime(event.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
return Container(
|
||||
@ -588,7 +523,8 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
),
|
||||
Text(
|
||||
event.tanggalPenyaluran != null
|
||||
? DateFormat('dd').format(event.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDateTime(
|
||||
event.tanggalPenyaluran!)
|
||||
: '--',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@ -640,8 +576,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
|
||||
|
||||
Widget _buildPenitipanCard(dynamic penitipan) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
Color statusColor;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
|
||||
class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
const DonaturJadwalDetailView({Key? key}) : super(key: key);
|
||||
const DonaturJadwalDetailView({super.key});
|
||||
|
||||
@override
|
||||
DonaturDashboardController get controller {
|
||||
@ -35,7 +35,6 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
_buildDetailSection(jadwal),
|
||||
_buildPelaksanaSection(jadwal),
|
||||
_buildStatusSection(jadwal),
|
||||
_buildActionSection(jadwal),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -126,8 +125,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!),
|
||||
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
@ -204,11 +202,26 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
),
|
||||
backgroundImage: jadwal.fotoPetugas != null &&
|
||||
jadwal.fotoPetugas.toString().isNotEmpty
|
||||
? NetworkImage(jadwal.fotoPetugas as String)
|
||||
: 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),
|
||||
Expanded(
|
||||
@ -254,50 +267,87 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
children: [
|
||||
const SectionHeader(title: 'Status Penyaluran'),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusTimeline(jadwal),
|
||||
_buildStatusCard(jadwal),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusTimeline(PenyaluranBantuanModel jadwal) {
|
||||
Widget _buildStatusCard(PenyaluranBantuanModel jadwal) {
|
||||
final status = jadwal.status;
|
||||
final bool isCompleted = status == 'SELESAI';
|
||||
final bool isCancelled = status == 'DIBATALKAN';
|
||||
final bool isInProgress = status == 'DALAM_PROSES';
|
||||
final bool isCompleted = status == 'TERLAKSANA';
|
||||
final bool isCancelled = status == 'BATALTERLAKSANA';
|
||||
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(
|
||||
children: [
|
||||
_buildTimelineItem(
|
||||
title: 'Dijadwalkan',
|
||||
date: jadwal.createdAt != null
|
||||
? DateFormat('dd MMM yyyy', 'id_ID').format(jadwal.createdAt!)
|
||||
: '-',
|
||||
isCompleted: true,
|
||||
isFirst: true,
|
||||
),
|
||||
_buildTimelineItem(
|
||||
title: 'Dalam Proses',
|
||||
date: isInProgress || isCompleted
|
||||
? jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('dd MMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
: '-'
|
||||
: '-',
|
||||
isCompleted: isInProgress || isCompleted,
|
||||
isCancelled: isCancelled,
|
||||
),
|
||||
_buildTimelineItem(
|
||||
title: 'Selesai',
|
||||
date: isCompleted
|
||||
? jadwal.tanggalSelesai != null
|
||||
? DateFormat('dd MMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalSelesai!)
|
||||
: '-'
|
||||
: '-',
|
||||
isCompleted: isCompleted,
|
||||
isCancelled: isCancelled,
|
||||
isLast: true,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: statusColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(statusIcon, color: statusColor, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusDetailItem(
|
||||
title: 'Tanggal Dijadwalkan',
|
||||
value: FormatHelper.formatDateIndonesian(jadwal.createdAt),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_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) ...[
|
||||
const SizedBox(height: 16),
|
||||
@ -333,7 +383,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
const SizedBox(height: 8),
|
||||
if (jadwal.tanggalPembatalan != null)
|
||||
Text(
|
||||
'Dibatalkan pada: ${DateFormat('dd MMMM yyyy', 'id_ID').format(jadwal.tanggalPembatalan!)}',
|
||||
'Dibatalkan pada: ${FormatHelper.formatDateIndonesian(jadwal.tanggalPembatalan)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red.shade700,
|
||||
@ -347,159 +397,29 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimelineItem({
|
||||
required String title,
|
||||
required String date,
|
||||
required bool isCompleted,
|
||||
bool isFirst = false,
|
||||
bool isLast = false,
|
||||
bool isCancelled = false,
|
||||
}) {
|
||||
Widget _buildStatusDetailItem(
|
||||
{required String title, required String value}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Column(
|
||||
children: [
|
||||
if (!isFirst)
|
||||
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,
|
||||
),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
@ -97,7 +98,7 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
for (var jadwal in controller.jadwalPenyaluran) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
String monthYear =
|
||||
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!);
|
||||
FormatHelper.formatDate(jadwal.tanggalPenyaluran!, format: 'MMMM');
|
||||
|
||||
if (!groupedJadwal.containsKey(monthYear)) {
|
||||
groupedJadwal[monthYear] = [];
|
||||
@ -110,9 +111,14 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
// Urutkan kunci (bulan) secara kronologis
|
||||
List<String> sortedMonths = groupedJadwal.keys.toList()
|
||||
..sort((a, b) {
|
||||
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
||||
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
|
||||
return dateA.compareTo(dateB);
|
||||
try {
|
||||
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
|
||||
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(
|
||||
@ -158,28 +164,27 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
|
||||
Widget _buildJadwalCard(dynamic jadwal) {
|
||||
final formattedDate = jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDateTime(jadwal.tanggalPenyaluran!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
String statusText = 'Akan Datang';
|
||||
String statusText = 'Dijadwalkan';
|
||||
Color statusColor = Colors.blue;
|
||||
|
||||
switch (jadwal.status) {
|
||||
case 'SELESAI':
|
||||
statusText = 'Selesai';
|
||||
case 'TERLAKSANA':
|
||||
statusText = 'Terlaksana';
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'DIBATALKAN':
|
||||
statusText = 'Dibatalkan';
|
||||
case 'BATALTERLAKSANA':
|
||||
statusText = 'Batal Terlaksana';
|
||||
statusColor = Colors.red;
|
||||
break;
|
||||
case 'DALAM_PROSES':
|
||||
statusText = 'Dalam Proses';
|
||||
statusColor = Colors.orange;
|
||||
case 'AKTIF':
|
||||
statusText = 'Aktif';
|
||||
statusColor = Colors.blue;
|
||||
break;
|
||||
default:
|
||||
statusText = 'Akan Datang';
|
||||
statusText = 'Dijadwalkan';
|
||||
statusColor = Colors.blue;
|
||||
}
|
||||
|
||||
@ -248,8 +253,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
),
|
||||
child: Text(
|
||||
jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('MMM', 'id_ID')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDate(
|
||||
jadwal.tanggalPenyaluran!,
|
||||
format: 'MMM')
|
||||
.toUpperCase()
|
||||
: 'TBD',
|
||||
style: const TextStyle(
|
||||
@ -265,8 +271,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
jadwal.tanggalPenyaluran != null
|
||||
? DateFormat('dd')
|
||||
.format(jadwal.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDate(
|
||||
jadwal.tanggalPenyaluran!,
|
||||
format: 'dd')
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
@ -459,11 +466,11 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
|
||||
|
||||
IconData _getStatusIcon(String? status) {
|
||||
switch (status) {
|
||||
case 'SELESAI':
|
||||
case 'TERLAKSANA':
|
||||
return Icons.check_circle;
|
||||
case 'DIBATALKAN':
|
||||
case 'BATALTERLAKSANA':
|
||||
return Icons.cancel;
|
||||
case 'DALAM_PROSES':
|
||||
case 'AKTIF':
|
||||
return Icons.timelapse;
|
||||
default:
|
||||
return Icons.event_available;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
|
||||
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
DonaturRiwayatPenitipanView({super.key});
|
||||
@ -60,8 +61,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||
final tanggal = item.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(item.tanggalPenitipan!)
|
||||
? FormatHelper.formatDateTime(item.tanggalPenitipan!)
|
||||
.toLowerCase()
|
||||
: '';
|
||||
|
||||
@ -214,8 +214,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
Widget _buildPenitipanCard(
|
||||
BuildContext context, dynamic penitipan, Color statusColor) {
|
||||
final formattedDate = penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia';
|
||||
|
||||
IconData statusIcon;
|
||||
@ -435,61 +434,6 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
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(
|
||||
AlertDialog(
|
||||
title: const Text('Detail Penitipan'),
|
||||
@ -509,8 +453,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
_buildInfoRow(
|
||||
'Tanggal Penitipan',
|
||||
penitipan.tanggalPenitipan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penitipan.tanggalPenitipan!)
|
||||
? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
|
||||
: 'Tanggal tidak tersedia',
|
||||
),
|
||||
_buildInfoRow(
|
||||
@ -520,8 +463,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
if (penitipan.tanggalVerifikasi != null)
|
||||
_buildInfoRow(
|
||||
'Tanggal Verifikasi',
|
||||
DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
||||
.format(penitipan.tanggalVerifikasi!),
|
||||
FormatHelper.formatDateTime(penitipan.tanggalVerifikasi!),
|
||||
),
|
||||
if (penitipan.deskripsi != null &&
|
||||
penitipan.deskripsi!.isNotEmpty)
|
||||
@ -543,8 +485,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
showFullScreenImage(penitipan.fotoBantuan!.first),
|
||||
onTap: () => ShowImageDialog.showFullScreen(
|
||||
context,
|
||||
penitipan.fotoBantuan!.first,
|
||||
),
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
@ -572,8 +516,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
showFullScreenImage(penitipan.fotoBuktiSerahTerima!),
|
||||
onTap: () => ShowImageDialog.showFullScreen(
|
||||
context,
|
||||
penitipan.fotoBuktiSerahTerima!,
|
||||
),
|
||||
child: Container(
|
||||
height: 200,
|
||||
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/data/models/stok_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_helper.dart';
|
||||
|
||||
class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
||||
const DonaturSkemaView({super.key});
|
||||
@ -549,14 +548,14 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
|
||||
|
||||
int days = difference.inDays;
|
||||
if (days > 0) {
|
||||
return 'Batas waktu: ${days} hari lagi';
|
||||
return 'Batas waktu: $days hari lagi';
|
||||
} else {
|
||||
int hours = difference.inHours;
|
||||
if (hours > 0) {
|
||||
return 'Batas waktu: ${hours} jam lagi';
|
||||
return 'Batas waktu: $hours jam lagi';
|
||||
} else {
|
||||
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
|
||||
return DateHelper.formatRupiah(nilai);
|
||||
return FormatHelper.formatRupiah(nilai);
|
||||
}
|
||||
|
||||
// Jika bukan uang, kembalikan nilai + satuan (jika ada)
|
||||
return '${jumlahDiterimaPerOrang} ${stokBantuan.satuan ?? ''}';
|
||||
return '$jumlahDiterimaPerOrang ${stokBantuan.satuan ?? ''}';
|
||||
}
|
||||
|
||||
String _formatRupiah(dynamic amount) {
|
||||
if (amount is num) {
|
||||
return DateHelper.formatRupiah(amount);
|
||||
return FormatHelper.formatRupiah(amount);
|
||||
} else if (amount is String) {
|
||||
try {
|
||||
double nilai = double.parse(amount);
|
||||
return DateHelper.formatRupiah(nilai);
|
||||
return FormatHelper.formatRupiah(nilai);
|
||||
} catch (e) {
|
||||
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(() {
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const Text('Dashboard Donatur');
|
||||
return const Text('Dashboard');
|
||||
case 1:
|
||||
return const Text('Skema Bantuan');
|
||||
case 2:
|
||||
@ -44,7 +44,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
||||
case 4:
|
||||
return const Text('Laporan Penyaluran');
|
||||
default:
|
||||
return const Text('Dashboard Donatur');
|
||||
return const Text('Dashboard');
|
||||
}
|
||||
}),
|
||||
leading: IconButton(
|
||||
@ -201,12 +201,20 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
child: (controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty)
|
||||
? Text(
|
||||
controller.nama.isNotEmpty
|
||||
? controller.nama
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -284,44 +292,175 @@ class DonaturView extends GetView<DonaturDashboardController> {
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
_buildMenuCategory('Menu Utama'),
|
||||
Obx(() => _buildMenuItem(
|
||||
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: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: const Text('Riwayat Donasi'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.info_outline,
|
||||
activeIcon: Icons.info,
|
||||
title: 'Tentang Kami',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi riwayat donasi
|
||||
Get.toNamed('/about');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Pengaturan'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implementasi pengaturan
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
_buildMenuItem(
|
||||
icon: Icons.logout,
|
||||
title: 'Keluar',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.logout();
|
||||
},
|
||||
isLogout: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} 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 'package:open_file/open_file.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
|
||||
@ -635,7 +634,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
fontSize: 12,
|
||||
color: PdfColors.blue900)),
|
||||
pw.Text(
|
||||
'Tanggal: ${DateFormat('dd MMMM yyyy').format(DateTime.now())}',
|
||||
'Tanggal: ${FormatHelper.formatDateTime(DateTime.now())}',
|
||||
style: pw.TextStyle(font: ttf, fontSize: 10),
|
||||
),
|
||||
],
|
||||
@ -708,8 +707,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
_buildPdfRow(
|
||||
'Tanggal Laporan',
|
||||
laporan.tanggalLaporan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
laporan.tanggalLaporan!)
|
||||
? FormatHelper.formatDateTime(laporan.tanggalLaporan!)
|
||||
: '-',
|
||||
ttf,
|
||||
ttfBold),
|
||||
@ -731,7 +729,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
_buildPdfRow(
|
||||
'Tanggal Penyaluran',
|
||||
penyaluran.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
penyaluran.tanggalPenyaluran!)
|
||||
: '-',
|
||||
ttf,
|
||||
@ -739,7 +737,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
_buildPdfRow(
|
||||
'Tanggal Selesai',
|
||||
penyaluran.tanggalSelesai != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
penyaluran.tanggalSelesai!)
|
||||
: '-',
|
||||
ttf,
|
||||
@ -902,7 +900,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
|
||||
final isUang = stokBantuan['is_uang'] == true;
|
||||
final formattedJumlah = isUang
|
||||
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}'
|
||||
? FormatHelper.formatRupiah(jumlah)
|
||||
: '$jumlah ${stokBantuan['satuan'] ?? ''}';
|
||||
|
||||
return pw.TableRow(
|
||||
@ -975,7 +973,7 @@ class LaporanPenyaluranController extends GetxController {
|
||||
final jumlahBantuan = penerima.jumlahBantuan ?? 0;
|
||||
|
||||
final formattedJumlah = isUang
|
||||
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlahBantuan)}'
|
||||
? FormatHelper.formatRupiah(jumlahBantuan)
|
||||
: '$jumlahBantuan ${penerima.satuan ?? ''}';
|
||||
|
||||
return pw.TableRow(
|
||||
|
@ -65,7 +65,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
||||
controller.selectedPenyaluran.value!
|
||||
.tanggalPenyaluran !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
? FormatHelper.formatDateTime(controller
|
||||
.selectedPenyaluran.value!.tanggalPenyaluran!)
|
||||
: '-',
|
||||
),
|
||||
@ -73,7 +73,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
||||
'Tanggal Selesai',
|
||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
? FormatHelper.formatDateTime(controller
|
||||
.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/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranView({super.key});
|
||||
@ -255,8 +254,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatusBadge(status: laporan.status ?? 'DRAFT'),
|
||||
// const SizedBox(width: 8),
|
||||
// StatusBadge(status: laporan.status ?? 'DRAFT'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -273,10 +272,11 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
Icons.calendar_today,
|
||||
'Tanggal',
|
||||
laporan.tanggalLaporan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
laporan.tanggalLaporan!)
|
||||
: '-',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildInfoItem(
|
||||
Icons.description,
|
||||
'Status',
|
||||
@ -538,8 +538,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
penyaluran.tanggalSelesai != null
|
||||
? DateFormat('dd/MM/yyyy')
|
||||
.format(penyaluran.tanggalSelesai!)
|
||||
? FormatHelper.formatDateTime(
|
||||
penyaluran.tanggalSelesai!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
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) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
DateTime jadwalDate =
|
||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||
FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||
|
||||
if (jadwalDate
|
||||
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
|
||||
@ -346,7 +346,7 @@ class CalendarViewWidget extends StatelessWidget {
|
||||
|
||||
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
|
||||
final String formattedDate =
|
||||
DateTimeHelper.formatDateIndonesian(appointment.startTime);
|
||||
FormatHelper.formatDateIndonesian(appointment.startTime);
|
||||
|
||||
// Dapatkan status dari ID jadwal
|
||||
String? status = _getStatusFromAppointmentId(appointment.id);
|
||||
|
@ -207,7 +207,7 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
|
||||
// Format tanggal dan waktu menggunakan helper
|
||||
String formattedDateTime =
|
||||
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
|
||||
FormatHelper.formatDateTime(jadwal.tanggalPenyaluran);
|
||||
|
||||
// Dapatkan nama lokasi dan kategori
|
||||
String lokasiName =
|
||||
|
@ -211,18 +211,16 @@ class DetailPenyaluranController extends GetxController {
|
||||
.eq('id', penerima.id!)
|
||||
.single();
|
||||
|
||||
if (penerimaData != null) {
|
||||
final String stokBantuanId = penerimaData['stok_bantuan_id'];
|
||||
final double jumlah = penerimaData['jumlah_bantuan'] is int
|
||||
? penerimaData['jumlah_bantuan'].toDouble()
|
||||
: penerimaData['jumlah_bantuan'];
|
||||
final String stokBantuanId = penerimaData['stok_bantuan_id'];
|
||||
final double jumlah = penerimaData['jumlah_bantuan'] is int
|
||||
? penerimaData['jumlah_bantuan'].toDouble()
|
||||
: penerimaData['jumlah_bantuan'];
|
||||
|
||||
// Kurangi stok dan catat riwayat
|
||||
final petugasId = _supabaseService.client.auth.currentUser?.id;
|
||||
if (petugasId != null) {
|
||||
await _supabaseService.kurangiStokDariPenyaluran(
|
||||
penerima.id!, stokBantuanId, jumlah, petugasId);
|
||||
}
|
||||
// Kurangi stok dan catat riwayat
|
||||
final petugasId = _supabaseService.client.auth.currentUser?.id;
|
||||
if (petugasId != null) {
|
||||
await _supabaseService.kurangiStokDariPenyaluran(
|
||||
penerima.id!, stokBantuanId, jumlah, petugasId);
|
||||
}
|
||||
|
||||
// Refresh data setelah konfirmasi berhasil
|
||||
|
@ -11,14 +11,21 @@ import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
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 {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
final SupabaseService _supabaseService = SupabaseService.to;
|
||||
late final JadwalUpdateService _jadwalUpdateService;
|
||||
late final StreamSubscription _jadwalUpdateSubscription;
|
||||
|
||||
SupabaseService get supabaseService => _supabaseService;
|
||||
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isLoadingStatusUpdate = false.obs;
|
||||
final RxBool isLokasiLoading = false.obs;
|
||||
|
||||
// Indeks kategori yang dipilih untuk filter
|
||||
final RxInt selectedCategoryIndex = 0.obs;
|
||||
@ -52,6 +59,21 @@ class JadwalPenyaluranController extends GetxController {
|
||||
@override
|
||||
void 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();
|
||||
loadPermintaanPenjadwalanData();
|
||||
loadLokasiPenyaluranData();
|
||||
@ -67,100 +89,444 @@ class JadwalPenyaluranController extends GetxController {
|
||||
searchController.dispose();
|
||||
// Hentikan timer jika ada
|
||||
_stopJadwalCheckTimer();
|
||||
// Berhenti berlangganan pembaruan jadwal
|
||||
_jadwalUpdateSubscription.cancel();
|
||||
// Batalkan pendaftaran controller
|
||||
_jadwalUpdateService.unregisterFromUpdates('JadwalPenyaluranController');
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Timer untuk memeriksa jadwal secara berkala
|
||||
Timer? _jadwalCheckTimer;
|
||||
Timer?
|
||||
_intensiveCheckTimer; // Timer untuk pengecekan intensif mendekati waktu penyaluran
|
||||
final RxBool _intensiveCheckActive = false.obs; // Status pengecekan intensif
|
||||
|
||||
void _startJadwalCheckTimer() {
|
||||
// Periksa jadwal setiap 1 menit
|
||||
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||
checkAndUpdateJadwalStatus();
|
||||
// Dengan fitur realtime yang sudah aktif, kita bisa mengurangi frekuensi polling
|
||||
// Cek setiap 30 detik sebagai fallback untuk realtime
|
||||
_jadwalCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (!isLoadingStatusUpdate.value) {
|
||||
checkAndUpdateJadwalStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Periksa jadwal segera saat aplikasi dimulai
|
||||
checkAndUpdateJadwalStatus();
|
||||
|
||||
// Log info untuk debugging
|
||||
print('Jadwal check timer started with 30 seconds interval');
|
||||
|
||||
// Mulai juga pengecekan jadwal yang akan datang
|
||||
_startUpcomingJadwalCheck();
|
||||
}
|
||||
|
||||
void _stopJadwalCheckTimer() {
|
||||
_jadwalCheckTimer?.cancel();
|
||||
_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
|
||||
Future<void> checkAndUpdateJadwalStatus() async {
|
||||
if (isLoadingStatusUpdate.value) return;
|
||||
|
||||
isLoadingStatusUpdate.value = true;
|
||||
print('Starting jadwal status check at ${DateTime.now()}');
|
||||
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
|
||||
List<PenyaluranBantuanModel> jadwalToUpdate = [];
|
||||
List<PenyaluranBantuanModel> jadwalTerlewat = [];
|
||||
// Kelompokkan jadwal yang perlu diperbarui untuk mengurangi jumlah operasi database
|
||||
final Map<String, String> jadwalUpdates = {};
|
||||
final List<PenyaluranBantuanModel> jadwalToUpdate = [];
|
||||
final List<PenyaluranBantuanModel> jadwalTerlewat = [];
|
||||
|
||||
for (var jadwal in jadwalAktif) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
final jadwalDateTime =
|
||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||
final jadwalDate = DateTime(
|
||||
jadwalDateTime.year,
|
||||
jadwalDateTime.month,
|
||||
jadwalDateTime.day,
|
||||
);
|
||||
print('Checking ${jadwalMendatang.length} upcoming schedules');
|
||||
|
||||
if (isSameDay(jadwalDate, today)) {
|
||||
if (now.isAfter(jadwalDateTime) ||
|
||||
now.isAtSameMomentAs(jadwalDateTime)) {
|
||||
if (jadwal.status == 'DIJADWALKAN') {
|
||||
if (now
|
||||
.isBefore(jadwalDateTime.add(const Duration(hours: 2)))) {
|
||||
await _supabaseService.updateJadwalStatus(
|
||||
jadwal.id!, 'AKTIF');
|
||||
jadwalToUpdate.add(jadwal);
|
||||
} else {
|
||||
await _supabaseService.updateJadwalStatus(
|
||||
jadwal.id!, 'BATALTERLAKSANA');
|
||||
jadwalTerlewat.add(jadwal);
|
||||
}
|
||||
} else if (jadwal.status == 'AKTIF') {
|
||||
if (now.isAfter(jadwalDateTime.add(const Duration(hours: 2)))) {
|
||||
await _supabaseService.updateJadwalStatus(
|
||||
jadwal.id!, 'BATALTERLAKSANA');
|
||||
jadwalTerlewat.add(jadwal);
|
||||
}
|
||||
// Proses semua jadwal yang perlu diperbarui
|
||||
for (var jadwal in jadwalMendatang) {
|
||||
if (jadwal.tanggalPenyaluran != null && jadwal.id != null) {
|
||||
final jadwalDate = jadwal.tanggalPenyaluran!;
|
||||
|
||||
// Log untuk debugging waktu pemeriksaan
|
||||
print(
|
||||
'Checking jadwal: ${jadwal.id} - ${jadwal.nama} scheduled for ${jadwal.tanggalPenyaluran}');
|
||||
print('Current time: $now, Jadwal time: $jadwalDate');
|
||||
|
||||
// Periksa apakah jadwal sudah melewati waktunya
|
||||
// Kita gunakan isAtSameMomentAs atau isAfter untuk menangkap dengan tepat
|
||||
if (now.isAfter(jadwalDate) || now.isAtSameMomentAs(jadwalDate)) {
|
||||
print('Jadwal time has passed/reached for ${jadwal.id}');
|
||||
|
||||
// Batasan 2 jam untuk status aktif
|
||||
final batasAktif = jadwalDate.add(const Duration(hours: 2));
|
||||
|
||||
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) {
|
||||
await loadJadwalData();
|
||||
// Update database hanya jika ada perubahan
|
||||
if (jadwalUpdates.isNotEmpty) {
|
||||
print('Batch updating ${jadwalUpdates.length} schedules');
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Gunakan batch update untuk meningkatkan efisiensi
|
||||
await _supabaseService.batchUpdateJadwalStatus(jadwalUpdates);
|
||||
|
||||
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),
|
||||
);
|
||||
// Perbarui data lokal
|
||||
await loadJadwalData();
|
||||
|
||||
// Beritahu seluruh aplikasi tentang pembaruan
|
||||
await _jadwalUpdateService.notifyJadwalUpdate();
|
||||
|
||||
// Kirim notifikasi untuk perubahan status jadwal
|
||||
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) {
|
||||
print('Error checking and updating jadwal status: $e');
|
||||
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))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Perbarui counter penyaluran setelah data dimuat
|
||||
_updatePenyaluranCounters();
|
||||
} catch (e) {
|
||||
print('Error loading jadwal data: $e');
|
||||
} finally {
|
||||
@ -220,6 +589,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
|
||||
Future<void> loadLokasiPenyaluranData() async {
|
||||
try {
|
||||
isLokasiLoading(true);
|
||||
final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
|
||||
if (lokasiData != null) {
|
||||
for (var lokasi in lokasiData) {
|
||||
@ -229,6 +599,8 @@ class JadwalPenyaluranController extends GetxController {
|
||||
}
|
||||
} catch (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 {
|
||||
isLoading.value = true;
|
||||
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);
|
||||
|
||||
// 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();
|
||||
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Jadwal berhasil diselesaikan',
|
||||
@ -359,15 +753,13 @@ class JadwalPenyaluranController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadJadwalData();
|
||||
await loadPermintaanPenjadwalanData();
|
||||
} catch (e) {
|
||||
print('Error refreshing data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
await Future.wait([
|
||||
loadJadwalData(),
|
||||
loadPermintaanPenjadwalanData(),
|
||||
loadLokasiPenyaluranData(),
|
||||
loadKategoriBantuanData(),
|
||||
loadSkemaBantuanData(),
|
||||
]);
|
||||
}
|
||||
|
||||
void changeCategory(int index) {
|
||||
@ -431,6 +823,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
'status_penerimaan': 'BELUMMENERIMA',
|
||||
'qr_code_hash': qrCodeHash,
|
||||
'jumlah_bantuan': jumlahDiterimaPerOrang,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
// Simpan data penerima ke database
|
||||
|
@ -96,10 +96,10 @@ class PelaksanaanPenyaluranController extends GetxController {
|
||||
? response['kategori_bantuan']['nama']
|
||||
: 'Tidak tersedia',
|
||||
'tanggal': penyaluranModel.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!)
|
||||
? FormatHelper.formatDateTime(penyaluranModel.tanggalPenyaluran!)
|
||||
: 'Tidak tersedia',
|
||||
'waktu': penyaluranModel.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
|
||||
? FormatHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
|
||||
: 'Tidak tersedia',
|
||||
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
|
||||
'status': penyaluranModel.status,
|
||||
|
@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
|
||||
);
|
||||
|
||||
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/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/services/jadwal_update_service.dart';
|
||||
|
||||
class PetugasDesaController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
@ -182,10 +183,22 @@ class PetugasDesaController extends GetxController {
|
||||
}
|
||||
_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();
|
||||
loadNotifikasiData();
|
||||
loadJadwalData();
|
||||
loadPenitipanData();
|
||||
loadJadwalData();
|
||||
loadNotifikasiData();
|
||||
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/services/supabase_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 {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
final SupabaseService _supabaseService = SupabaseService.to;
|
||||
late final CounterService _counterService;
|
||||
late final JadwalUpdateService _jadwalUpdateService;
|
||||
late StreamSubscription _jadwalUpdateSubscription;
|
||||
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
@ -67,18 +71,47 @@ class PetugasDesaDashboardController extends GetxController {
|
||||
}
|
||||
_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();
|
||||
loadDashboardData();
|
||||
loadNotifikasiData();
|
||||
loadJadwalAktif();
|
||||
loadJadwalHariIni();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Berhenti berlangganan pembaruan jadwal
|
||||
_jadwalUpdateSubscription.cancel();
|
||||
// Batalkan pendaftaran controller
|
||||
_jadwalUpdateService
|
||||
.unregisterFromUpdates('PetugasDesaDashboardController');
|
||||
searchController.dispose();
|
||||
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
|
||||
Future<void> loadUserProfile() async {
|
||||
try {
|
||||
@ -155,14 +188,14 @@ class PetugasDesaDashboardController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadJadwalAktif() async {
|
||||
Future<void> loadJadwalHariIni() async {
|
||||
try {
|
||||
final jadwalData = await _supabaseService.getJadwalAktif();
|
||||
if (jadwalData != null) {
|
||||
jadwalHariIni.value = jadwalData;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading jadwal hari ini: $e');
|
||||
print('Error loading jadwal data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +206,7 @@ class PetugasDesaDashboardController extends GetxController {
|
||||
loadUserProfile(),
|
||||
loadDashboardData(),
|
||||
loadNotifikasiData(),
|
||||
loadJadwalAktif(),
|
||||
loadJadwalHariIni(),
|
||||
]);
|
||||
} catch (e) {
|
||||
print('Error refreshing data: $e');
|
||||
|
@ -221,15 +221,25 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 35,
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
backgroundImage: penerima['foto_profil'] != null
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
backgroundImage: penerima['foto_profil'] != null &&
|
||||
penerima['foto_profil'].toString().isNotEmpty
|
||||
? NetworkImage(penerima['foto_profil'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
size: 35,
|
||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||
child: (penerima['foto_profil'] == null ||
|
||||
penerima['foto_profil'].toString().isEmpty)
|
||||
? Text(
|
||||
penerima['nama_lengkap'] != null
|
||||
? penerima['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -435,13 +445,24 @@ class PenerimaSearchDelegate extends SearchDelegate {
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
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'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
color: AppTheme.primaryColor,
|
||||
child: (penerima['foto_profil'] == null ||
|
||||
penerima['foto_profil'].toString().isEmpty)
|
||||
? Text(
|
||||
penerima['nama_lengkap'] != null
|
||||
? penerima['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
@ -33,6 +33,58 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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
|
||||
FadeInAnimation(
|
||||
child: GreetingHeader(
|
||||
@ -83,7 +135,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Jadwal Penyaluran',
|
||||
'Jadwal Penyaluran Hari Ini',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -130,19 +182,25 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
||||
final DateTime tanggal =
|
||||
DateTime.parse(jadwal['tanggal_penyaluran']);
|
||||
final String formattedDate =
|
||||
DateTimeHelper.formatDateTime(tanggal);
|
||||
FormatHelper.formatDateTime(tanggal);
|
||||
final kategoriBantuan =
|
||||
jadwal['kategori_bantuan'] as Map<String, dynamic>;
|
||||
final lokasiPenyaluran =
|
||||
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
|
||||
|
||||
return ScheduleCard(
|
||||
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
|
||||
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
|
||||
dateTime: formattedDate,
|
||||
isToday: true,
|
||||
onTap: () => Get.toNamed(Routes.detailPenyaluran,
|
||||
parameters: {'id': jadwal['id']}),
|
||||
return Column(
|
||||
children: [
|
||||
if (index > 0) const SizedBox(height: 10),
|
||||
ScheduleCard(
|
||||
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
|
||||
location:
|
||||
lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
|
||||
dateTime: formattedDate,
|
||||
isToday: true,
|
||||
onTap: () => Get.toNamed(Routes.detailPenyaluran,
|
||||
parameters: {'id': jadwal['id']}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -391,8 +449,10 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
||||
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
|
||||
final status = penerima['status'] ?? 'AKTIF';
|
||||
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(
|
||||
String name, String nik, String status, String id, TextTheme textTheme) {
|
||||
Widget _buildRecipientItem(String name, String nik, String status, String id,
|
||||
TextTheme textTheme, String? fotoProfil) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
@ -428,7 +488,20 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
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),
|
||||
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/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
|
||||
class DetailDonaturView extends GetView<DonaturController> {
|
||||
const DetailDonaturView({super.key});
|
||||
@ -359,7 +360,7 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
Icons.calendar_today,
|
||||
'Terdaftar Sejak',
|
||||
donatur.createdAt != null
|
||||
? DateTimeHelper.formatDate(donatur.createdAt!)
|
||||
? FormatHelper.formatDateTime(donatur.createdAt!)
|
||||
: 'Tidak diketahui',
|
||||
),
|
||||
],
|
||||
@ -514,7 +515,8 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
|
||||
final isUang = penitipan.isUang == true;
|
||||
final tanggal = penitipan.createdAt != null
|
||||
? DateTimeHelper.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy')
|
||||
? FormatHelper.formatDateTime(penitipan.createdAt!,
|
||||
format: 'dd MMM yyyy')
|
||||
: 'Tanggal tidak diketahui';
|
||||
|
||||
String nilaiDonasi = '';
|
||||
@ -626,7 +628,7 @@ class DetailDonaturView extends GetView<DonaturController> {
|
||||
getPetugasDesaNama: (String? id) =>
|
||||
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
|
||||
showFullScreenImage: (String imageUrl) {
|
||||
DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl);
|
||||
ShowImageDialog.showFullScreen(Get.context!, imageUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -107,14 +107,24 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
child: CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: penerima['foto_profil'] != null
|
||||
backgroundImage: penerima['foto_profil'] != null &&
|
||||
penerima['foto_profil'].toString().isNotEmpty
|
||||
? NetworkImage(penerima['foto_profil'])
|
||||
: null,
|
||||
child: penerima['foto_profil'] == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
size: 60,
|
||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||
child: (penerima['foto_profil'] == null ||
|
||||
penerima['foto_profil'].toString().isEmpty)
|
||||
? Text(
|
||||
penerima['nama_lengkap'] != null
|
||||
? penerima['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor.withOpacity(0.7),
|
||||
fontSize: 36,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -507,7 +517,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
|
||||
child: _buildInfoItem(
|
||||
Icons.calendar_today,
|
||||
'Tanggal Penerimaan',
|
||||
DateTimeHelper.formatDateTime(tanggalPenerimaan),
|
||||
FormatHelper.formatDateTime(tanggalPenerimaan),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
@ -1,13 +1,12 @@
|
||||
import 'package:flutter/material.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/tindakan_pengaduan_model.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/utils/format_helper.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/section_header.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:timeline_tile/timeline_tile.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/text_input.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> {
|
||||
const DetailPengaduanView({super.key});
|
||||
@ -1092,8 +1091,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
||||
child: Row(
|
||||
children: tindakan.buktiTindakan!.map((bukti) {
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
showFullScreenImage(context, bukti),
|
||||
onTap: () => ShowImageDialog.showFullScreen(
|
||||
context, bukti),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -1190,8 +1189,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
tindakan.tanggalTindakan != null
|
||||
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
|
||||
.format(tindakan.tanggalTindakan!)
|
||||
? FormatHelper.formatDateTime(
|
||||
tindakan.tanggalTindakan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
@ -1669,9 +1668,11 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => showFullScreenImage(
|
||||
stateContext,
|
||||
buktiTindakanPaths[index]),
|
||||
onTap: () => ShowImageDialog
|
||||
.showFullScreen(
|
||||
stateContext,
|
||||
buktiTindakanPaths[
|
||||
index]),
|
||||
child: Container(
|
||||
width: 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) {
|
||||
return Card(
|
||||
elevation: 3,
|
||||
@ -2348,8 +2292,7 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
pengaduan.tanggalPengaduan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(pengaduan.tanggalPengaduan!)
|
||||
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
@ -2376,7 +2319,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: GestureDetector(
|
||||
onTap: () => _showFullScreenImage(context, url),
|
||||
onTap: () =>
|
||||
ShowImageDialog.showFullScreen(context, url),
|
||||
child: Container(
|
||||
width: 120,
|
||||
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,
|
||||
'Tanggal Penyaluran',
|
||||
penyaluran.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
penyaluran.tanggalPenyaluran!)
|
||||
: 'Belum dijadwalkan',
|
||||
AppTheme.secondaryColor),
|
||||
@ -280,7 +280,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
Icons.event_available,
|
||||
'Tanggal Selesai',
|
||||
penyaluran.tanggalSelesai != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
penyaluran.tanggalSelesai!)
|
||||
: '-',
|
||||
AppTheme.secondaryColor),
|
||||
@ -1065,19 +1065,30 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
backgroundColor: sudahMenerima
|
||||
? statusColor.withOpacity(0.15)
|
||||
: Colors.grey.shade50,
|
||||
child: Text(
|
||||
warga != null && warga['nama_lengkap'] != null
|
||||
? warga['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: sudahMenerima ? statusColor : Colors.grey.shade700,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
backgroundImage: warga != null &&
|
||||
warga['foto_profil'] != null &&
|
||||
warga['foto_profil'].toString().isNotEmpty
|
||||
? NetworkImage(warga['foto_profil'])
|
||||
: null,
|
||||
child: (warga == null ||
|
||||
warga['foto_profil'] == null ||
|
||||
warga['foto_profil'].toString().isEmpty)
|
||||
? Text(
|
||||
warga != null && warga['nama_lengkap'] != null
|
||||
? 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),
|
||||
@ -1621,19 +1632,28 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: statusColor.withOpacity(0.2),
|
||||
child: Text(
|
||||
warga != null && warga['nama_lengkap'] != null
|
||||
? warga['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
backgroundImage: warga != null &&
|
||||
warga['foto_profil'] != null &&
|
||||
warga['foto_profil'].toString().isNotEmpty
|
||||
? NetworkImage(warga['foto_profil'])
|
||||
: null,
|
||||
child: (warga == null ||
|
||||
warga['foto_profil'] == null ||
|
||||
warga['foto_profil'].toString().isEmpty)
|
||||
? Text(
|
||||
warga != null && warga['nama_lengkap'] != null
|
||||
? warga['nama_lengkap']
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@ -1753,7 +1773,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
if (penerima.tanggalPenerimaan != null)
|
||||
_buildInfoRow(
|
||||
'Tanggal Penerimaan',
|
||||
DateTimeHelper.formatDate(
|
||||
FormatHelper.formatDateTime(
|
||||
penerima.tanggalPenerimaan!)),
|
||||
if (penerima.jumlahBantuan != null)
|
||||
_buildInfoRow('Jumlah Bantuan',
|
||||
@ -1946,7 +1966,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
_buildInfoRow('Status', 'Batal Terlaksana'),
|
||||
if (penyaluran.tanggalSelesai != null)
|
||||
_buildInfoRow('Tanggal Pembatalan',
|
||||
DateTimeHelper.formatDateTime(penyaluran.tanggalSelesai!)),
|
||||
FormatHelper.formatDateTime(penyaluran.tanggalSelesai!)),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Alasan Pembatalan:',
|
||||
@ -2126,7 +2146,7 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
_buildInfoRow(
|
||||
'Tanggal Laporan',
|
||||
controller.laporan.value?.tanggalLaporan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
controller.laporan.value!.tanggalLaporan!)
|
||||
: '-',
|
||||
),
|
||||
|
@ -198,7 +198,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
'Tempat, Tanggal Lahir',
|
||||
warga?['tempat_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'),
|
||||
const Divider(),
|
||||
|
||||
@ -236,18 +236,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
|
||||
|
||||
String tanggalWaktuPenyaluran = '';
|
||||
if (widget.tanggalPenyaluran != null) {
|
||||
final tanggal = DateTimeHelper.formatDate(widget.tanggalPenyaluran!);
|
||||
final waktuMulai = DateTimeHelper.formatTime(widget.tanggalPenyaluran!);
|
||||
final waktuSelesai = DateTimeHelper.formatTime(
|
||||
final tanggal = FormatHelper.formatDateTime(widget.tanggalPenyaluran!);
|
||||
final waktuMulai = FormatHelper.formatTime(widget.tanggalPenyaluran!);
|
||||
final waktuSelesai = FormatHelper.formatTime(
|
||||
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
|
||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||
} else if (penerima.penyaluranBantuan != null &&
|
||||
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
|
||||
final tanggalPenyaluran =
|
||||
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
|
||||
final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran);
|
||||
final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran);
|
||||
final waktuSelesai = DateTimeHelper.formatTime(
|
||||
final tanggal = FormatHelper.formatDateTime(tanggalPenyaluran);
|
||||
final waktuMulai = FormatHelper.formatTime(tanggalPenyaluran);
|
||||
final waktuSelesai = FormatHelper.formatTime(
|
||||
tanggalPenyaluran.add(const Duration(hours: 1)));
|
||||
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
|
||||
} else {
|
||||
|
@ -44,7 +44,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
final lastUpdate = DateTime
|
||||
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
|
||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
||||
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
@ -280,7 +280,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
|
||||
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
@ -320,7 +320,7 @@ class PengaduanView extends GetView<PengaduanController> {
|
||||
// Format tanggal menggunakan DateTimeHelper
|
||||
String formattedDate = '';
|
||||
if (item.tanggalPengaduan != null) {
|
||||
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
|
||||
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
|
||||
}
|
||||
|
||||
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/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
@ -72,7 +73,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
context,
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Menunggu',
|
||||
value: DateTimeHelper.formatNumber(
|
||||
value: FormatHelper.formatNumber(
|
||||
controller.jumlahMenunggu.value),
|
||||
color: Colors.orange,
|
||||
),
|
||||
@ -82,7 +83,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
context,
|
||||
icon: Icons.check_circle,
|
||||
title: 'Terverifikasi',
|
||||
value: DateTimeHelper.formatNumber(
|
||||
value: FormatHelper.formatNumber(
|
||||
controller.jumlahTerverifikasi.value),
|
||||
color: Colors.green,
|
||||
),
|
||||
@ -92,8 +93,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
context,
|
||||
icon: Icons.cancel,
|
||||
title: 'Ditolak',
|
||||
value: DateTimeHelper.formatNumber(
|
||||
controller.jumlahDitolak.value),
|
||||
value:
|
||||
FormatHelper.formatNumber(controller.jumlahDitolak.value),
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
@ -219,7 +220,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
||||
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
@ -360,7 +361,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
DateTimeHelper.formatDate(item.createdAt),
|
||||
FormatHelper.formatDateTime(item.createdAt),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade700,
|
||||
fontStyle: FontStyle.italic,
|
||||
@ -380,15 +381,27 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
radius: 20,
|
||||
child: Text(
|
||||
donaturNama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
backgroundImage: item.donatur != null &&
|
||||
item.donatur!.fotoProfil != null &&
|
||||
item.donatur!.fotoProfil!.isNotEmpty
|
||||
? 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),
|
||||
Expanded(
|
||||
@ -546,8 +559,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isUang
|
||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
@ -947,7 +960,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
kategoriSatuan: kategoriSatuan,
|
||||
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
|
||||
showFullScreenImage: (String imageUrl) {
|
||||
DetailPenitipanDialog.showFullScreenImage(context, imageUrl);
|
||||
ShowImageDialog.showFullScreen(context, imageUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -992,7 +1005,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
return Obx(() {
|
||||
final lastUpdate = controller.lastUpdateTime.value;
|
||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
||||
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
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/calendar_view_widget.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> {
|
||||
const PenyaluranView({super.key});
|
||||
@ -41,13 +42,20 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
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,
|
||||
floatingActionButton: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tombol untuk menambah jadwal penyaluran
|
||||
FloatingActionButton.extended(
|
||||
heroTag: 'tambahJadwal',
|
||||
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
|
||||
_buildJadwalSummary(Get.context!),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tombol untuk mengelola lokasi penyaluran
|
||||
_buildLokasiPenyaluranSection(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 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:
|
||||
return const Text('Stok Bantuan');
|
||||
default:
|
||||
return const Text('Petugas Desa');
|
||||
return const Text('Dashboard');
|
||||
}
|
||||
}),
|
||||
leading: IconButton(
|
||||
@ -223,14 +223,23 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white70,
|
||||
backgroundImage: controller.profilePhotoUrl != null
|
||||
backgroundImage: controller.profilePhotoUrl != null &&
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
child: (controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty)
|
||||
? Text(
|
||||
controller.nama.isNotEmpty
|
||||
? controller.nama
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 30,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -396,6 +405,16 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
_buildMenuItem(
|
||||
icon: Icons.info_outline,
|
||||
activeIcon: Icons.info,
|
||||
title: 'Tentang Kami',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/about');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.logout,
|
||||
title: 'Keluar',
|
||||
@ -411,7 +430,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
|
||||
'© ${DateTime.now().year} DisalurKita',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
|
@ -43,7 +43,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
final lastUpdate = DateTime.now();
|
||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
||||
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
@ -135,7 +135,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
|
||||
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
@ -154,9 +154,9 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
|
||||
// Format tanggal menggunakan DateTimeHelper
|
||||
String formattedDate = '';
|
||||
if (item.tanggalPengaduan != null) {
|
||||
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
|
||||
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
|
||||
} else if (item.createdAt != null) {
|
||||
formattedDate = DateTimeHelper.formatDate(item.createdAt);
|
||||
formattedDate = FormatHelper.formatDateTime(item.createdAt);
|
||||
}
|
||||
|
||||
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/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
|
||||
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
const RiwayatPenitipanView({super.key});
|
||||
@ -47,7 +48,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
|
||||
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
|
||||
final tanggal =
|
||||
DateTimeHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
|
||||
FormatHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
|
||||
|
||||
return donaturNama.contains(searchText) ||
|
||||
kategoriNama.contains(searchText) ||
|
||||
@ -99,7 +100,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
||||
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
@ -113,7 +114,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
|
||||
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
@ -126,7 +127,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Update: ${DateTimeHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
|
||||
'Update: ${FormatHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
@ -262,7 +263,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
DateTimeHelper.formatDate(item.createdAt),
|
||||
FormatHelper.formatDateTime(item.createdAt),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade700,
|
||||
fontStyle: FontStyle.italic,
|
||||
@ -282,17 +283,26 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
radius: 20,
|
||||
child: Text(
|
||||
donaturNama.isNotEmpty
|
||||
? donaturNama.substring(0, 1).toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
backgroundColor: statusColor.withOpacity(0.2),
|
||||
backgroundImage: item.donatur != null &&
|
||||
item.donatur!.fotoProfil != null &&
|
||||
item.donatur!.fotoProfil!.isNotEmpty
|
||||
? 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: statusColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@ -422,8 +432,8 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isUang
|
||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
@ -579,20 +589,20 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
_buildDetailItem(
|
||||
'Jumlah',
|
||||
isUang
|
||||
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
|
||||
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
|
||||
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
|
||||
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
|
||||
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
|
||||
_buildDetailItem(
|
||||
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
|
||||
_buildDetailItem(
|
||||
'Tanggal Penitipan',
|
||||
DateTimeHelper.formatDateTime(item.tanggalPenitipan,
|
||||
FormatHelper.formatDateTime(item.tanggalPenitipan,
|
||||
defaultValue: 'Tidak ada tanggal'),
|
||||
),
|
||||
if (item.tanggalVerifikasi != null)
|
||||
_buildDetailItem(
|
||||
'Tanggal Verifikasi',
|
||||
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
|
||||
FormatHelper.formatDateTime(item.tanggalVerifikasi),
|
||||
),
|
||||
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
|
||||
_buildDetailItem(
|
||||
@ -600,7 +610,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
controller.getPetugasDesaNama(item.petugasDesaId),
|
||||
),
|
||||
_buildDetailItem('Tanggal Dibuat',
|
||||
DateTimeHelper.formatDateTime(item.createdAt)),
|
||||
FormatHelper.formatDateTime(item.createdAt)),
|
||||
if (item.alasanPenolakan != null &&
|
||||
item.alasanPenolakan!.isNotEmpty)
|
||||
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
|
||||
@ -626,8 +636,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showFullScreenImage(
|
||||
context, item.fotoBantuan![index]);
|
||||
ShowImageDialog.show(
|
||||
context,
|
||||
item.fotoBantuan![index],
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
@ -677,8 +689,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showFullScreenImage(
|
||||
context, item.fotoBantuan![index]);
|
||||
ShowImageDialog.show(
|
||||
context,
|
||||
item.fotoBantuan![index],
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
@ -721,8 +735,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_showFullScreenImage(
|
||||
context, item.fotoBuktiSerahTerima!);
|
||||
ShowImageDialog.show(
|
||||
context,
|
||||
item.fotoBuktiSerahTerima!,
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
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) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
|
@ -52,7 +52,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
.getKategoriBantuanName(item.kategoriBantuanId)
|
||||
.toLowerCase();
|
||||
final tanggal =
|
||||
DateTimeHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
|
||||
FormatHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
|
||||
|
||||
return nama.contains(searchText) ||
|
||||
deskripsi.contains(searchText) ||
|
||||
@ -105,7 +105,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${DateTimeHelper.formatNumber(filteredList.length)} item',
|
||||
'${FormatHelper.formatNumber(filteredList.length)} item',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
@ -119,7 +119,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
|
||||
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
@ -132,7 +132,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Update: ${DateTimeHelper.formatDateTimeWithHour(DateTime.now())}',
|
||||
'Update: ${FormatHelper.formatDateTimeWithHour(DateTime.now())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
@ -305,7 +305,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
child: _buildInfoItem(
|
||||
Icons.event,
|
||||
'Tanggal',
|
||||
DateTimeHelper.formatDateTime(item.tanggalPenyaluran,
|
||||
FormatHelper.formatDateTime(item.tanggalPenyaluran,
|
||||
format: 'dd MMM yyyy HH:mm'),
|
||||
Theme.of(context).textTheme,
|
||||
),
|
||||
@ -316,17 +316,57 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
_buildInfoItem(
|
||||
Icons.people_outline,
|
||||
'Jumlah Penerima',
|
||||
'${DateTimeHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
|
||||
'${FormatHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
|
||||
Theme.of(context).textTheme,
|
||||
),
|
||||
if (item.alasanPembatalan != null &&
|
||||
item.alasanPembatalan!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(
|
||||
Icons.info_outline,
|
||||
'Alasan Pembatalan',
|
||||
item.alasanPembatalan!,
|
||||
Theme.of(context).textTheme,
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
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),
|
||||
|
@ -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/utils/format_helper.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
|
||||
class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
const RiwayatStokView({super.key});
|
||||
@ -353,7 +354,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
@ -543,7 +544,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
riwayat.createdAt != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
riwayat.createdAt!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
@ -598,7 +599,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
padding: const EdgeInsets.only(left: 44),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_showImageDialog(context, riwayat.fotoBukti!),
|
||||
ShowImageDialog.show(context, riwayat.fotoBukti!),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
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}) {
|
||||
// Reset form
|
||||
controller.resetForm();
|
||||
@ -1152,7 +1062,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
Widget _buildPenitipanDetail(
|
||||
BuildContext context, Map<String, dynamic> data) {
|
||||
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
|
||||
@ -1357,7 +1267,8 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
padding: EdgeInsets.only(
|
||||
right: index < fotoBantuan.length - 1 ? 8.0 : 0),
|
||||
child: InkWell(
|
||||
onTap: () => _showImageDialog(context, imageUrl),
|
||||
onTap: () =>
|
||||
ShowImageDialog.show(context, imageUrl),
|
||||
child: Container(
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
@ -1442,7 +1353,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
Widget _buildPenerimaanDetail(
|
||||
BuildContext context, Map<String, dynamic> data) {
|
||||
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
|
||||
@ -1646,7 +1557,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: () => _showImageDialog(context, buktiPenerimaan),
|
||||
onTap: () => ShowImageDialog.show(context, buktiPenerimaan),
|
||||
child: Container(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
|
@ -156,7 +156,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rp ${DateTimeHelper.formatNumber(controller.totalDanaBantuan.value)}',
|
||||
'Rp ${FormatHelper.formatNumber(controller.totalDanaBantuan.value)}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -512,8 +512,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
),
|
||||
Text(
|
||||
item.isUang == true
|
||||
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
|
||||
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
|
||||
? 'Rp ${FormatHelper.formatNumber(item.totalStok)}'
|
||||
: '${FormatHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
@ -549,7 +549,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.updatedAt != null
|
||||
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}'
|
||||
? 'Diperbarui: ${FormatHelper.formatDateTimeWithHour(item.updatedAt!)}'
|
||||
: 'Tidak ada data pembaruan',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
@ -984,8 +984,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isUang
|
||||
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
|
||||
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
|
||||
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
@ -1175,8 +1175,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
stok.isUang == true
|
||||
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
|
||||
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
|
||||
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
@ -1240,7 +1240,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||
return Obx(() {
|
||||
final lastUpdate = controller.lastUpdateTime.value;
|
||||
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
|
||||
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
|
||||
|
||||
return Padding(
|
||||
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() {
|
||||
return CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 70,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
child: Obx(() {
|
||||
final user = controller.user.value;
|
||||
final roleData = controller.roleData.value;
|
||||
|
||||
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) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
'assets/images/logo-disalurkita.png',
|
||||
width: 150,
|
||||
height: 150,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: AppTheme.primaryColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
@ -62,24 +59,25 @@ class _SplashViewState extends State<SplashView> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Aplikasi Penyaluran',
|
||||
'DisalurKita',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Bantuan Sosial',
|
||||
'Salurkan dengan Pasti, Pantau dengan Bukti',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,16 +1,16 @@
|
||||
import 'package:flutter/material.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/tindakan_pengaduan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:timeline_tile/timeline_tile.dart';
|
||||
import 'package:image_picker/image_picker.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 'dart:io';
|
||||
import 'package:penyaluran_app/app/widgets/widgets.dart';
|
||||
|
||||
class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||
const WargaDetailPengaduanView({super.key});
|
||||
@ -670,8 +670,7 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
pengaduan.tanggalPengaduan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(pengaduan.tanggalPengaduan!)
|
||||
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
@ -1309,8 +1308,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||
child: Row(
|
||||
children: tindakan.buktiTindakan!.map((bukti) {
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
showFullScreenImage(context, bukti),
|
||||
onTap: () => ShowImageDialog.showFullScreen(
|
||||
context, bukti),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -1407,8 +1406,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
tindakan.tanggalTindakan != null
|
||||
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
|
||||
.format(tindakan.tanggalTindakan!)
|
||||
? FormatHelper.formatDateTime(
|
||||
tindakan.tanggalTindakan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
@ -1429,183 +1428,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
void showFullScreenImage(BuildContext context, String imageUrl) {
|
||||
// Buat controller untuk InteractiveViewer
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
void _showFullScreenImage(BuildContext context, String imagePath) {
|
||||
ShowImageDialog.showFullScreen(context, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2056,8 +1880,7 @@ class _TambahTindakanPengaduanViewState
|
||||
}
|
||||
|
||||
void _showFullScreenImage(BuildContext context, String imagePath) {
|
||||
final wargaDetailView = Get.find<WargaDetailPengaduanView>();
|
||||
wargaDetailView.showFullScreenImage(context, imagePath);
|
||||
ShowImageDialog.showFullScreen(context, imagePath);
|
||||
}
|
||||
|
||||
Future<void> _simpanTindakan() async {
|
||||
@ -2078,22 +1901,6 @@ class _TambahTindakanPengaduanViewState
|
||||
});
|
||||
|
||||
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
|
||||
Get.back(); // Kembali ke halaman sebelumnya
|
||||
Get.snackbar(
|
||||
|
@ -11,12 +11,12 @@ class FormPengaduanView extends StatefulWidget {
|
||||
final List<File>? selectedImages;
|
||||
|
||||
const FormPengaduanView({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.uidPenerimaan,
|
||||
this.judul,
|
||||
this.deskripsi,
|
||||
this.selectedImages,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormPengaduanView> createState() => _FormPengaduanViewState();
|
||||
@ -219,7 +219,7 @@ class _FormPengaduanViewState extends State<FormPengaduanView> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
|
||||
class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
@ -23,6 +23,54 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatisticSection(),
|
||||
@ -90,10 +138,17 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.blue.shade700,
|
||||
size: 30,
|
||||
? Text(
|
||||
controller.nama.isNotEmpty
|
||||
? controller.nama
|
||||
.substring(0, 1)
|
||||
.toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -417,12 +472,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
}
|
||||
|
||||
Widget _buildPenerimaanSummary() {
|
||||
final currencyFormat = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
double totalUang = 0;
|
||||
Map<String, double> totalNonUang = {};
|
||||
|
||||
@ -494,7 +543,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
icon: Icons.attach_money,
|
||||
color: Colors.green,
|
||||
title: 'Total Bantuan Uang',
|
||||
value: currencyFormat.format(totalUang),
|
||||
value: FormatHelper.formatRupiah(totalUang),
|
||||
),
|
||||
if (totalNonUang.isNotEmpty) ...[
|
||||
if (totalUang > 0)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/pengaduan_model.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:qr_flutter/qr_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@ -131,17 +131,11 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) {
|
||||
final currencyFormat = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
||||
String formattedJumlah = '';
|
||||
if (penyaluran.jumlahBantuan != null) {
|
||||
if (penyaluran.isUang == true) {
|
||||
formattedJumlah = currencyFormat.format(penyaluran.jumlahBantuan);
|
||||
formattedJumlah = FormatHelper.formatRupiah(penyaluran.jumlahBantuan);
|
||||
} else {
|
||||
formattedJumlah =
|
||||
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
|
||||
@ -390,8 +384,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
icon: Icons.calendar_today,
|
||||
title: 'Tanggal Penerimaan',
|
||||
value: penyaluran.tanggalPenerimaan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(penyaluran.tanggalPenerimaan!)
|
||||
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||
: 'Belum diterima',
|
||||
statusColor: null,
|
||||
),
|
||||
@ -400,8 +393,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
icon: Icons.access_time,
|
||||
title: 'Waktu Penerimaan',
|
||||
value: penyaluran.tanggalPenerimaan != null
|
||||
? DateFormat('HH:mm', 'id_ID')
|
||||
.format(penyaluran.tanggalPenerimaan!)
|
||||
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||
: 'Belum diterima',
|
||||
statusColor: null,
|
||||
),
|
||||
@ -758,8 +750,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
icon: Icons.update,
|
||||
title: 'Terakhir Diperbarui',
|
||||
value: penyaluran.tanggalPenerimaan != null
|
||||
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
||||
.format(penyaluran.tanggalPenerimaan!)
|
||||
? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
|
||||
: 'Tidak tersedia',
|
||||
statusColor: null,
|
||||
),
|
||||
@ -1394,7 +1385,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Pengaduan Terdaftar',
|
||||
'Pengaduan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -1547,8 +1538,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
pengaduan.tanggalPengaduan != null
|
||||
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID')
|
||||
.format(pengaduan.tanggalPengaduan!)
|
||||
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
|
||||
: 'Tanggal tidak tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
|
@ -1,12 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.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> {
|
||||
const WargaPengaduanView({super.key});
|
||||
@ -380,7 +375,7 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.tanggalPengaduan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
? FormatHelper.formatDateTime(
|
||||
item.tanggalPengaduan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
|
@ -20,13 +20,13 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
title: Obx(() {
|
||||
switch (controller.activeTabIndex.value) {
|
||||
case 0:
|
||||
return const Text('Dashboard Warga');
|
||||
return const Text('Dashboard');
|
||||
case 1:
|
||||
return const Text('Penerimaan Bantuan');
|
||||
case 2:
|
||||
return const Text('Pengaduan');
|
||||
default:
|
||||
return const Text('Dashboard Warga');
|
||||
return const Text('Dashboard');
|
||||
}
|
||||
}),
|
||||
leading: IconButton(
|
||||
@ -164,16 +164,19 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.white70,
|
||||
backgroundImage: controller.profilePhotoUrl != null &&
|
||||
controller.profilePhotoUrl!.isNotEmpty
|
||||
? NetworkImage(controller.profilePhotoUrl!)
|
||||
backgroundImage: controller.fotoProfil.value.isNotEmpty
|
||||
? NetworkImage(controller.fotoProfil.value)
|
||||
: null,
|
||||
child: controller.profilePhotoUrl == null ||
|
||||
controller.profilePhotoUrl!.isEmpty
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
child: controller.fotoProfil.isEmpty
|
||||
? Text(
|
||||
controller.nama.isNotEmpty
|
||||
? controller.nama.substring(0, 1).toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -292,6 +295,15 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
controller.refreshData();
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.info_outline,
|
||||
activeIcon: Icons.info,
|
||||
title: 'Tentang Kami',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/about');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
icon: Icons.logout,
|
||||
title: 'Keluar',
|
||||
@ -307,7 +319,7 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
|
||||
'© ${DateTime.now().year} DisalurKita',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
|
@ -1,4 +1,5 @@
|
||||
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/register_donatur_view.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/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_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/detail_penyaluran_page.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/views/qr_scanner_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/donatur_binding.dart';
|
||||
import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart';
|
||||
@ -64,6 +67,11 @@ class AppPages {
|
||||
page: () => const RegisterDonaturView(),
|
||||
binding: AuthBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.forgotPassword,
|
||||
page: () => const ForgotPasswordView(),
|
||||
binding: AuthBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.wargaDashboard,
|
||||
page: () => WargaView(),
|
||||
@ -92,6 +100,11 @@ class AppPages {
|
||||
page: () => const PetugasDesaView(),
|
||||
binding: PetugasDesaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.about,
|
||||
page: () => const AboutView(),
|
||||
binding: AboutBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.permintaanPenjadwalan,
|
||||
page: () => const PermintaanPenjadwalanView(),
|
||||
@ -137,6 +150,11 @@ class AppPages {
|
||||
page: () => const TambahPenyaluranView(),
|
||||
binding: PetugasDesaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.tambahLokasiPenyaluran,
|
||||
page: () => const TambahLokasiPenyaluranView(),
|
||||
binding: PetugasDesaBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.detailPenyaluran,
|
||||
page: () => DetailPenyaluranPage(),
|
||||
|
@ -6,6 +6,7 @@ abstract class Routes {
|
||||
static const login = _Paths.login;
|
||||
static const register = _Paths.register;
|
||||
static const registerDonatur = _Paths.registerDonatur;
|
||||
static const forgotPassword = _Paths.forgotPassword;
|
||||
static const wargaDashboard = _Paths.wargaDashboard;
|
||||
static const wargaPenerimaan = _Paths.wargaPenerimaan;
|
||||
static const wargaPengaduan = _Paths.wargaPengaduan;
|
||||
@ -23,10 +24,12 @@ abstract class Routes {
|
||||
static const konfirmasiPenerima = _Paths.konfirmasiPenerima;
|
||||
static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran;
|
||||
static const profile = _Paths.profile;
|
||||
static const about = _Paths.about;
|
||||
static const riwayatPenitipan = _Paths.riwayatPenitipan;
|
||||
static const daftarDonatur = _Paths.daftarDonatur;
|
||||
static const detailDonatur = _Paths.detailDonatur;
|
||||
static const tambahPenyaluran = _Paths.tambahPenyaluran;
|
||||
static const tambahLokasiPenyaluran = _Paths.tambahLokasiPenyaluran;
|
||||
static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran;
|
||||
static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran;
|
||||
static const laporanPenyaluran = _Paths.laporanPenyaluran;
|
||||
@ -51,6 +54,7 @@ abstract class _Paths {
|
||||
static const login = '/login';
|
||||
static const register = '/register';
|
||||
static const registerDonatur = '/register-donatur';
|
||||
static const forgotPassword = '/forgot-password';
|
||||
static const wargaDashboard = '/warga-dashboard';
|
||||
static const wargaPenerimaan = '/warga-penerimaan';
|
||||
static const wargaPengaduan = '/warga-pengaduan';
|
||||
@ -68,10 +72,12 @@ abstract class _Paths {
|
||||
static const konfirmasiPenerima = '/daftar-penerima/konfirmasi';
|
||||
static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran';
|
||||
static const profile = '/profile';
|
||||
static const about = '/about';
|
||||
static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan';
|
||||
static const daftarDonatur = '/daftar-donatur';
|
||||
static const detailDonatur = '/daftar-donatur/detail';
|
||||
static const tambahPenyaluran = '/tambah-penyaluran';
|
||||
static const tambahLokasiPenyaluran = '/tambah-lokasi-penyaluran';
|
||||
static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran';
|
||||
static const detailPenerimaPenyaluran = '/detail-penerima-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 {
|
||||
final now = DateTime.now();
|
||||
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));
|
||||
|
||||
// Konversi ke UTC untuk query ke database
|
||||
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
|
||||
final weekUtc = week.toUtc().toIso8601String();
|
||||
|
||||
final response = await client
|
||||
.from('penyaluran_bantuan')
|
||||
.select('*')
|
||||
.gte('tanggal_penyaluran', tomorrowUtc)
|
||||
.lt('tanggal_penyaluran', weekUtc)
|
||||
.inFilter('status', ['DIJADWALKAN']);
|
||||
.gte('tanggal_penyaluran', today)
|
||||
.lt('tanggal_penyaluran', week)
|
||||
.inFilter('status', ['DIJADWALKAN']).order('tanggal_penyaluran',
|
||||
ascending: true);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
@ -651,15 +647,128 @@ class SupabaseService extends GetxService {
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui status jadwal
|
||||
Future<void> updateJadwalStatus(String jadwalId, String status) async {
|
||||
Future<void> updateJadwalStatus(String jadwalId, String newStatus) async {
|
||||
try {
|
||||
await client.from('penyaluran_bantuan').update({
|
||||
'status': status,
|
||||
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
||||
'status': newStatus,
|
||||
'updated_at': DateTime.now().toUtc().toIso8601String()
|
||||
}).eq('id', jadwalId);
|
||||
|
||||
print('Jadwal status updated: $jadwalId -> $newStatus');
|
||||
} catch (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')
|
||||
.eq('id', penitipanId);
|
||||
|
||||
if (response == null || response.isEmpty) {
|
||||
if (response.isEmpty) {
|
||||
throw 'Data penitipan tidak ditemukan';
|
||||
}
|
||||
|
||||
@ -1930,8 +2039,8 @@ class SupabaseService extends GetxService {
|
||||
}
|
||||
|
||||
if (jenisPerubahan != null) {
|
||||
filterString += (filterString.isNotEmpty ? ',' : '') +
|
||||
'jenis_perubahan.eq.$jenisPerubahan';
|
||||
filterString +=
|
||||
'${filterString.isNotEmpty ? ',' : ''}jenis_perubahan.eq.$jenisPerubahan';
|
||||
}
|
||||
|
||||
final response = await client.from('riwayat_stok').select('''
|
||||
@ -2006,7 +2115,7 @@ class SupabaseService extends GetxService {
|
||||
print('Stok berhasil ditambahkan dari penitipan');
|
||||
} catch (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');
|
||||
} catch (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 = '';
|
||||
if (fotoBuktiPath.isNotEmpty) {
|
||||
final String fileName =
|
||||
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
||||
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
|
||||
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||
fileName,
|
||||
File(fotoBuktiPath),
|
||||
@ -2125,7 +2234,7 @@ class SupabaseService extends GetxService {
|
||||
print('Stok berhasil ditambahkan secara manual');
|
||||
} catch (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 = '';
|
||||
if (fotoBuktiPath.isNotEmpty) {
|
||||
final String fileName =
|
||||
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
||||
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
|
||||
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||
fileName,
|
||||
File(fotoBuktiPath),
|
||||
@ -2198,7 +2307,7 @@ class SupabaseService extends GetxService {
|
||||
print('Stok berhasil dikurangi secara manual');
|
||||
} catch (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
|
||||
/// tanggal dan waktu.
|
||||
class DateTimeHelper {
|
||||
class FormatHelper {
|
||||
/// Mengkonversi DateTime dari UTC ke timezone lokal
|
||||
static DateTime toLocalDateTime(DateTime utcDateTime) {
|
||||
return utcDateTime.toLocal();
|
||||
@ -70,7 +70,6 @@ class DateTimeHelper {
|
||||
static String formatDateTime(
|
||||
DateTime? dateTime, {
|
||||
String format = 'dd MMM yyyy HH:mm',
|
||||
String locale = 'id_ID',
|
||||
String defaultValue = 'Belum ditentukan',
|
||||
}) {
|
||||
if (dateTime == null) return defaultValue;
|
||||
@ -78,7 +77,7 @@ class DateTimeHelper {
|
||||
// Pastikan tanggal dan waktu dalam timezone lokal
|
||||
final localDateTime = toLocalDateTime(dateTime);
|
||||
try {
|
||||
return DateFormat(format, locale).format(localDateTime);
|
||||
return DateFormat(format).format(localDateTime);
|
||||
} catch (e) {
|
||||
print('Error formatting date time: $e');
|
||||
return localDateTime.toString(); // Fallback to basic format
|
||||
@ -197,8 +196,10 @@ class DateTimeHelper {
|
||||
final String tanggal = localDateTime.day.toString().padLeft(2, '0');
|
||||
final String bulan = namaBulan[localDateTime.month - 1];
|
||||
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
|
||||
|
@ -38,14 +38,20 @@ class AppDrawer extends StatelessWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage:
|
||||
avatar != null ? NetworkImage(avatar!) : null,
|
||||
child: avatar == null
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
backgroundImage: avatar != null && avatar!.isNotEmpty
|
||||
? NetworkImage(avatar!)
|
||||
: null,
|
||||
child: (avatar == null || avatar!.isEmpty)
|
||||
? Text(
|
||||
nama.isNotEmpty
|
||||
? nama.substring(0, 1).toUpperCase()
|
||||
: '?',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
@ -1,6 +1,6 @@
|
||||
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/utils/format_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||
|
||||
class BantuanCard extends StatelessWidget {
|
||||
@ -17,17 +17,11 @@ class BantuanCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currencyFormat = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
// Format jumlah bantuan berdasarkan tipe (uang atau bukan)
|
||||
String formattedJumlah = '';
|
||||
if (item.jumlahBantuan != null) {
|
||||
if (item.isUang == true) {
|
||||
formattedJumlah = currencyFormat.format(item.jumlahBantuan);
|
||||
formattedJumlah = FormatHelper.formatRupiah(item.jumlahBantuan);
|
||||
} else {
|
||||
formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}';
|
||||
}
|
||||
@ -120,8 +114,8 @@ class BantuanCard extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.tanggalPenerimaan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(item.tanggalPenerimaan!)
|
||||
? FormatHelper.formatDateTime(
|
||||
item.tanggalPenerimaan!)
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
@ -373,8 +367,8 @@ class BantuanCard extends StatelessWidget {
|
||||
Icons.calendar_today,
|
||||
'Tanggal:',
|
||||
item.tanggalPenerimaan != null
|
||||
? DateFormat('dd MMMM yyyy', 'id_ID')
|
||||
.format(item.tanggalPenerimaan!)
|
||||
? FormatHelper.formatDateTime(
|
||||
item.tanggalPenerimaan!)
|
||||
: '-',
|
||||
),
|
||||
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/utils/format_helper.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
|
||||
///
|
||||
@ -48,7 +49,7 @@ class DetailPenitipanDialog {
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Tanggal Penitipan',
|
||||
DateTimeHelper.formatDateTime(
|
||||
FormatHelper.formatDateTime(
|
||||
item.tanggalPenitipan ?? item.createdAt),
|
||||
),
|
||||
_buildInfoRow(
|
||||
@ -63,7 +64,7 @@ class DetailPenitipanDialog {
|
||||
if (item.tanggalVerifikasi != null)
|
||||
_buildInfoRow(
|
||||
'Tanggal Verifikasi',
|
||||
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
|
||||
FormatHelper.formatDateTime(item.tanggalVerifikasi),
|
||||
),
|
||||
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
|
||||
_buildInfoRow('Deskripsi', item.deskripsi!),
|
||||
@ -143,50 +144,7 @@ class DetailPenitipanDialog {
|
||||
|
||||
/// Menampilkan gambar dalam layar penuh
|
||||
static void showFullScreenImage(BuildContext context, String imageUrl) {
|
||||
Navigator.of(context).push(
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
ShowImageDialog.showFullScreen(context, imageUrl);
|
||||
}
|
||||
|
||||
/// 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
|
||||
export 'dialogs/detail_penitipan_dialog.dart';
|
||||
export 'dialogs/confirmation_dialog.dart';
|
||||
export 'dialogs/show_image_dialog.dart';
|
||||
|
||||
// Indicators
|
||||
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:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:syncfusion_localizations/syncfusion_localizations.dart';
|
||||
import 'package:penyaluran_app/app/services/notification_service.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@ -27,11 +28,26 @@ void main() async {
|
||||
|
||||
// Inisialisasi service
|
||||
Future<void> initServices() async {
|
||||
await Get.putAsync(() => SupabaseService().init());
|
||||
await Get.putAsync(() => AuthService().init());
|
||||
print('Initializing services...');
|
||||
// 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
|
||||
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 {
|
||||
@ -40,7 +56,7 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp(
|
||||
title: 'Penerimaan App',
|
||||
title: 'DisalurKita',
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.light, // Default ke tema terang
|
||||
@ -60,6 +76,7 @@ class MyApp extends StatelessWidget {
|
||||
Locale('id', 'ID'), // Indonesia
|
||||
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 open_file_mac
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
18
pubspec.lock
@ -670,7 +670,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
@ -837,6 +837,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -94,6 +94,8 @@ dependencies:
|
||||
uuid: ^4.5.1
|
||||
# Library untuk cached image
|
||||
cached_network_image: ^3.3.1
|
||||
share_plus: ^10.1.4
|
||||
path: ^1.9.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.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>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|