h-1 lebaran

This commit is contained in:
Khafidh Fuadi
2025-03-30 14:45:16 +07:00
parent c008020705
commit 5aaeb58d2b
91 changed files with 9448 additions and 3756 deletions

View File

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

View File

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

View File

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

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~ ?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
| |
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  <08>ϕ<EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2{ zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt  Ұ<EFBFBD><EFBFBD><EFBFBD>2  <20><><EFBFBD><EFBFBD><EFBFBD>2{
y y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD> wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json  Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
~ ~
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  <08>ϕ<EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m |D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json  Ұ<EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
k k
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  <08>ϕ<EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja  Ұ<EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
o o
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  <08>ϕ<EFBFBD><EFBFBD>2v mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt  Ұ<EFBFBD><EFBFBD><EFBFBD>2v
t t
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  <08>ϕ<EFBFBD><EFBFBD>2 rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt  Ұ<EFBFBD><EFBFBD><EFBFBD>2
K <20><><EFBFBD><EFBFBD><EFBFBD>2w K <20><><EFBFBD><EFBFBD><EFBFBD>2w
u u
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  Ұ<><D2B0><EFBFBD>2 { sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json  Ұ<><D2B0><EFBFBD>2 {
y y
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  Ұ<><D2B0><EFBFBD>2 wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin  Ұ<><D2B0><EFBFBD>2
<EFBFBD> <EFBFBD>
 
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  Ұ<><D2B0><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2t }D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt  Ұ<><D2B0><EFBFBD>2 <18> <20><><EFBFBD><EFBFBD><EFBFBD>2t
r r
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  Ұ<><D2B0><EFBFBD>2 pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json  Ұ<><D2B0><EFBFBD>2
 ( <20><><EFBFBD><EFBFBD><EFBFBD>2y  ( <20><><EFBFBD><EFBFBD><EFBFBD>2y

View File

@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="penyaluran_app" android:label="DisalurKita"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@ -47,5 +47,24 @@
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
<!-- Konfigurasi untuk url_launcher -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/pdf" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="image/*" />
</intent>
</queries> </queries>
</manifest> </manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

108
assets/sql/README.md Normal file
View 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';
```

View 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;
$$;

View File

@ -51,5 +51,11 @@
<string>Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima</string> <string>Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Aplikasi memerlukan akses mikrofon untuk merekam video</string> <string>Aplikasi memerlukan akses mikrofon untuk merekam video</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>http</string>
<string>https</string>
<string>file</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -12,6 +12,7 @@ class LokasiPenyaluranModel {
final double? latitude; final double? latitude;
final double? longitude; final double? longitude;
final String? petugasDesaId; // Referensi ke PetugasDesa final String? petugasDesaId; // Referensi ke PetugasDesa
final bool isLokasiTitip; // Field baru untuk menentukan lokasi penitipan
final DateTime createdAt; final DateTime createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
@ -27,6 +28,7 @@ class LokasiPenyaluranModel {
this.latitude, this.latitude,
this.longitude, this.longitude,
this.petugasDesaId, this.petugasDesaId,
this.isLokasiTitip = false, // Nilai default false
required this.createdAt, required this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -49,6 +51,7 @@ class LokasiPenyaluranModel {
latitude: json["latitude"]?.toDouble(), latitude: json["latitude"]?.toDouble(),
longitude: json["longitude"]?.toDouble(), longitude: json["longitude"]?.toDouble(),
petugasDesaId: json["petugas_desa_id"], petugasDesaId: json["petugas_desa_id"],
isLokasiTitip: json["is_lokasi_titip"] ?? false,
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null updatedAt: json["updated_at"] == null
? null ? null
@ -67,6 +70,7 @@ class LokasiPenyaluranModel {
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
"petugas_desa_id": petugasDesaId, "petugas_desa_id": petugasDesaId,
"is_lokasi_titip": isLokasiTitip,
"created_at": createdAt.toIso8601String(), "created_at": createdAt.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };

View File

@ -71,6 +71,14 @@ class PenyaluranBantuanModel {
return null; return null;
} }
// Mendapatkan foto petugas dari relasi petugas
String? get fotoPetugas {
if (petugas != null && petugas!['foto_profil'] != null) {
return petugas!['foto_profil'];
}
return null;
}
factory PenyaluranBantuanModel.fromRawJson(String str) => factory PenyaluranBantuanModel.fromRawJson(String str) =>
PenyaluranBantuanModel.fromJson(json.decode(str)); PenyaluranBantuanModel.fromJson(json.decode(str));
@ -126,4 +134,49 @@ class PenyaluranBantuanModel {
"created_at": createdAt?.toUtc().toIso8601String(), "created_at": createdAt?.toUtc().toIso8601String(),
"updated_at": updatedAt?.toUtc().toIso8601String(), "updated_at": updatedAt?.toUtc().toIso8601String(),
}; };
// Metode copyWith untuk membuat salinan objek dengan perubahan tertentu
PenyaluranBantuanModel copyWith({
String? id,
String? nama,
String? deskripsi,
String? petugasId,
String? skemaId,
String? lokasiPenyaluranId,
String? kategoriBantuanId,
int? jumlahPenerima,
DateTime? tanggalPenyaluran,
String? status,
String? alasanPembatalan,
DateTime? tanggalPembatalan,
DateTime? tanggalSelesai,
DateTime? createdAt,
DateTime? updatedAt,
Map<String, dynamic>? lokasiPenyaluran,
Map<String, dynamic>? kategori,
Map<String, dynamic>? petugas,
int? jumlahBantuan,
}) {
return PenyaluranBantuanModel(
id: id ?? this.id,
nama: nama ?? this.nama,
deskripsi: deskripsi ?? this.deskripsi,
petugasId: petugasId ?? this.petugasId,
skemaId: skemaId ?? this.skemaId,
lokasiPenyaluranId: lokasiPenyaluranId ?? this.lokasiPenyaluranId,
kategoriBantuanId: kategoriBantuanId ?? this.kategoriBantuanId,
jumlahPenerima: jumlahPenerima ?? this.jumlahPenerima,
tanggalPenyaluran: tanggalPenyaluran ?? this.tanggalPenyaluran,
status: status ?? this.status,
alasanPembatalan: alasanPembatalan ?? this.alasanPembatalan,
tanggalPembatalan: tanggalPembatalan ?? this.tanggalPembatalan,
tanggalSelesai: tanggalSelesai ?? this.tanggalSelesai,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lokasiPenyaluran: lokasiPenyaluran ?? this.lokasiPenyaluran,
kategori: kategori ?? this.kategori,
petugas: petugas ?? this.petugas,
jumlahBantuan: jumlahBantuan ?? this.jumlahBantuan,
);
}
} }

View File

@ -457,4 +457,12 @@ class AuthProvider {
Future<void> markNotificationAsRead(int notificationId) async { Future<void> markNotificationAsRead(int notificationId) async {
await _supabaseService.markNotificationAsRead(notificationId); await _supabaseService.markNotificationAsRead(notificationId);
} }
// Metode untuk reset password
Future<void> resetPasswordForEmail(String email, {String? redirectTo}) async {
await _supabaseService.client.auth.resetPasswordForEmail(
email,
redirectTo: redirectTo,
);
}
} }

View 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
}
}

View 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,
),
],
),
),
],
);
}
}

View File

@ -50,6 +50,10 @@ class AuthController extends GetxController {
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxBool isWargaProfileComplete = false.obs; final RxBool isWargaProfileComplete = false.obs;
// Variable untuk mengontrol visibility password
final RxBool isPasswordHidden = true.obs;
final RxBool isConfirmPasswordHidden = true.obs;
// Flag untuk menandai apakah sudah melakukan pengambilan data profil // Flag untuk menandai apakah sudah melakukan pengambilan data profil
final RxBool _hasLoadedProfile = false.obs; final RxBool _hasLoadedProfile = false.obs;
@ -376,6 +380,65 @@ class AuthController extends GetxController {
return null; return null;
} }
// Metode untuk reset password
Future<void> resetPassword(String email) async {
if (email.isEmpty) {
Get.snackbar(
'Error',
'Email tidak boleh kosong',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
if (!GetUtils.isEmail(email)) {
Get.snackbar(
'Error',
'Email tidak valid',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
try {
isLoading.value = true;
// Menggunakan Supabase untuk reset password
await _authProvider.resetPasswordForEmail(
email,
redirectTo: 'penyaluranapp://reset-password',
);
Get.snackbar(
'Sukses',
'Instruksi reset password telah dikirim ke email Anda',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// Kembali ke halaman login
Future.delayed(const Duration(seconds: 3), () {
Get.offNamed(Routes.login);
});
} catch (e) {
print('Error saat reset password: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat mengirim reset password. Silakan coba lagi nanti.',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Metode untuk refresh data user setelah update profil // Metode untuk refresh data user setelah update profil
Future<void> refreshUserData() async { Future<void> refreshUserData() async {
try { try {
@ -543,4 +606,14 @@ class AuthController extends GetxController {
noHpController.clear(); noHpController.clear();
jenisController.clear(); jenisController.clear();
} }
// Metode untuk toggle visibility password
void togglePasswordVisibility() {
isPasswordHidden.value = !isPasswordHidden.value;
}
// Metode untuk toggle visibility konfirmasi password
void toggleConfirmPasswordVisibility() {
isConfirmPasswordHidden.value = !isConfirmPasswordHidden.value;
}
} }

View 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,
),
),
],
),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -10,140 +10,360 @@ class LoginView extends GetView<AuthController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( body: Container(
child: Padding( decoration: const BoxDecoration(
padding: const EdgeInsets.all(20.0), gradient: LinearGradient(
child: SingleChildScrollView( begin: Alignment.topCenter,
child: Form( end: Alignment.bottomCenter,
key: controller.loginFormKey, colors: [Color(0xFFE3F2FD), Colors.white],
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.stretch, ),
children: [ child: SafeArea(
const SizedBox(height: 50), child: Padding(
// Logo atau Judul padding: const EdgeInsets.all(24.0),
const Center( child: SingleChildScrollView(
child: Text( child: Form(
'Penyaluran App', key: controller.loginFormKey,
style: TextStyle( child: Column(
fontSize: 28, crossAxisAlignment: CrossAxisAlignment.stretch,
fontWeight: FontWeight.bold, children: [
color: Colors.blue, // Logo
Center(
child: Image.asset(
'assets/images/logo-disalurkita.png',
width: 250,
height: 250,
), ),
), ),
),
const SizedBox(height: 10), const Center(
const Center( child: Text(
child: Text( 'Masuk ke akun Anda',
'Masuk ke akun Anda', style: TextStyle(
style: TextStyle( fontSize: 16,
fontSize: 16, color: Color(0xFF546E7A),
color: Colors.grey, fontWeight: FontWeight.w500,
),
), ),
), ),
), const SizedBox(height: 20),
const SizedBox(height: 50),
// Email Field // Email Field
TextFormField( Container(
controller: controller.emailController, decoration: BoxDecoration(
keyboardType: TextInputType.emailAddress, color: Colors.white,
decoration: InputDecoration( borderRadius: BorderRadius.circular(15),
labelText: 'Email', boxShadow: [
prefixIcon: const Icon(Icons.email), BoxShadow(
border: OutlineInputBorder( color: Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(10), blurRadius: 10,
spreadRadius: 1,
),
],
),
child: TextFormField(
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
labelText: 'Email',
prefixIcon:
const Icon(Icons.email, color: Color(0xFF1565C0)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide:
const BorderSide(color: Colors.red, width: 1.5),
),
fillColor: Colors.white,
filled: true,
),
validator: controller.validateEmail,
), ),
), ),
validator: controller.validateEmail, const SizedBox(height: 20),
),
const SizedBox(height: 20),
// Password Field // Password Field
TextFormField( Container(
controller: controller.passwordController, decoration: BoxDecoration(
obscureText: true, color: Colors.white,
decoration: InputDecoration( borderRadius: BorderRadius.circular(15),
labelText: 'Password', boxShadow: [
prefixIcon: const Icon(Icons.lock), BoxShadow(
border: OutlineInputBorder( color: Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(10), blurRadius: 10,
spreadRadius: 1,
),
],
), ),
child: Obx(() => TextFormField(
controller: controller.passwordController,
obscureText: controller.isPasswordHidden.value,
decoration: InputDecoration(
hintText: 'Masukkan password Anda',
labelText: 'Password',
prefixIcon: const Icon(Icons.lock,
color: Color(0xFF1565C0)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Colors.red, width: 1.5),
),
fillColor: Colors.white,
filled: true,
suffixIcon: IconButton(
onPressed: () {
controller.isPasswordHidden.value =
!controller.isPasswordHidden.value;
},
icon: Icon(
!controller.isPasswordHidden.value
? Icons.visibility
: Icons.visibility_off,
color: const Color(0xFF78909C),
),
splashRadius: 20,
),
),
validator: controller.validatePassword,
)),
), ),
validator: controller.validatePassword, const SizedBox(height: 10),
),
const SizedBox(height: 10),
// Forgot Password // Forgot Password
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () => Get.toNamed(Routes.forgotPassword),
// Implementasi lupa password style: TextButton.styleFrom(
}, foregroundColor: const Color(0xFF1565C0),
child: const Text('Lupa Password?'), ),
), child: const Text(
), 'Lupa Password?',
const SizedBox(height: 20), style: TextStyle(
fontWeight: FontWeight.w600,
// Login Button
Obx(() => ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
), ),
), ),
child: controller.isLoading.value ),
? const SpinKitThreeBounce( ),
color: Colors.white, const SizedBox(height: 20),
size: 24,
) // Login Button
: const Text( Obx(() => Container(
'MASUK', decoration: BoxDecoration(
style: TextStyle( borderRadius: BorderRadius.circular(15),
fontSize: 16, boxShadow: [
fontWeight: FontWeight.bold, BoxShadow(
color: const Color(0xFF1565C0).withOpacity(0.3),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: const Color(0xFF1565C0),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 0,
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Text(
'MASUK',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
)),
const SizedBox(height: 30),
// Divider
Row(
children: [
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey.withOpacity(0.1),
Colors.grey.withOpacity(0.5),
],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'ATAU',
style: TextStyle(
color: Color(0xFF546E7A),
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey.withOpacity(0.1),
Colors.grey.withOpacity(0.5),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
),
),
],
),
const SizedBox(height: 30),
// Register Donatur Button
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: OutlinedButton(
onPressed: () => Get.toNamed(Routes.registerDonatur),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
foregroundColor: const Color(0xFF1565C0),
side: const BorderSide(
color: Color(0xFF1565C0), width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: const Text(
'DAFTAR SEBAGAI DONATUR',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 40),
// Informasi Pendaftaran Warga
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: const Color(0xFFFFCC80), width: 1),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Color(0xFFFFCC80),
shape: BoxShape.circle,
),
child: const Icon(
Icons.info_outline,
color: Color(0xFFE65100),
size: 24,
), ),
), ),
)), const SizedBox(width: 12),
const SizedBox(height: 20), const Expanded(
child: Text(
// Divider 'Informasi Penting',
const Row( style: TextStyle(
children: [ fontSize: 16,
Expanded(child: Divider()), fontWeight: FontWeight.bold,
Padding( color: Color(0xFFE65100),
padding: EdgeInsets.symmetric(horizontal: 16.0), ),
child: ),
Text('ATAU', style: TextStyle(color: Colors.grey)), ),
), ],
Expanded(child: Divider()), ),
], const SizedBox(height: 12),
), const Text(
const SizedBox(height: 20), 'Pendaftaran warga hanya dapat dilakukan melalui aplikasi verifikasi data warga. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
style: TextStyle(
// Register Donatur Button fontSize: 14,
OutlinedButton( color: Color(0xFF424242),
onPressed: () => Get.toNamed(Routes.registerDonatur), height: 1.5,
style: OutlinedButton.styleFrom( ),
padding: const EdgeInsets.symmetric(vertical: 15), ),
shape: RoundedRectangleBorder( ],
borderRadius: BorderRadius.circular(10),
),
side: const BorderSide(color: Colors.blue),
),
child: const Text(
'DAFTAR SEBAGAI DONATUR',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
), ),
), ),
), const SizedBox(height: 30),
],
// Footer
Center(
child: Text(
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Color(0xFF90A4AE),
),
),
),
],
),
), ),
), ),
), ),

View File

@ -11,8 +11,16 @@ class RegisterDonaturView extends GetView<AuthController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Daftar Sebagai Donatur'), title: const Text('Daftar Donatur'),
centerTitle: true,
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(15),
),
),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
@ -23,29 +31,60 @@ class RegisterDonaturView extends GetView<AuthController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 20),
// Logo atau Judul
const Center(
child: Text(
'Daftar Donatur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
const SizedBox(height: 10), const SizedBox(height: 10),
const Center( // Header dengan icon dan judul
child: Text( Container(
'Isi data untuk mendaftar sebagai donatur', padding: const EdgeInsets.all(15),
style: TextStyle( decoration: BoxDecoration(
fontSize: 16, color: Colors.blue.shade50,
color: Colors.grey, borderRadius: BorderRadius.circular(15),
), ),
child: Column(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 120,
height: 120,
),
const Text(
'Daftar Sebagai Donatur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 4),
const Text(
'Bergabunglah dengan kami untuk membantu mereka yang membutuhkan',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.blueGrey,
),
),
],
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 20),
// Step indicator
const Row(
children: [
Icon(Icons.person_add, color: Colors.blue),
SizedBox(width: 10),
Text(
'Informasi Akun',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 15),
const Divider(),
const SizedBox(height: 10),
// Nama Lengkap // Nama Lengkap
TextFormField( TextFormField(
@ -53,9 +92,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.name, keyboardType: TextInputType.name,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nama Lengkap', labelText: 'Nama Lengkap',
prefixIcon: const Icon(Icons.person), hintText: 'Masukkan nama lengkap Anda',
border: OutlineInputBorder( prefixIcon: const Icon(Icons.person, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
), ),
), ),
validator: controller.validateDonaturNama, validator: controller.validateDonaturNama,
@ -68,9 +120,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Email', labelText: 'Email',
prefixIcon: const Icon(Icons.email), hintText: 'contoh@email.com',
border: OutlineInputBorder( prefixIcon: const Icon(Icons.email, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
), ),
), ),
validator: controller.validateEmail, validator: controller.validateEmail,
@ -78,34 +143,101 @@ class RegisterDonaturView extends GetView<AuthController> {
const SizedBox(height: 15), const SizedBox(height: 15),
// Password // Password
TextFormField( Obx(() => TextFormField(
controller: controller.passwordController, controller: controller.passwordController,
obscureText: true, obscureText: controller.isPasswordHidden.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Password',
prefixIcon: const Icon(Icons.lock), hintText: 'Minimal 8 karakter',
border: OutlineInputBorder( prefixIcon:
borderRadius: BorderRadius.circular(10), const Icon(Icons.lock, color: Colors.blue),
), suffixIcon: IconButton(
), icon: Icon(
validator: controller.validatePassword, controller.isPasswordHidden.value
), ? Icons.visibility_off
: Icons.visibility,
color: Colors.blue,
),
onPressed: () =>
controller.togglePasswordVisibility(),
),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validatePassword,
)),
const SizedBox(height: 15), const SizedBox(height: 15),
// Confirm Password // Confirm Password
TextFormField( Obx(() => TextFormField(
controller: controller.confirmPasswordController, controller: controller.confirmPasswordController,
obscureText: true, obscureText: controller.isConfirmPasswordHidden.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Konfirmasi Password', labelText: 'Konfirmasi Password',
prefixIcon: const Icon(Icons.lock_outline), hintText: 'Masukkan password yang sama',
border: OutlineInputBorder( prefixIcon: const Icon(Icons.lock_outline,
borderRadius: BorderRadius.circular(10), color: Colors.blue),
suffixIcon: IconButton(
icon: Icon(
controller.isConfirmPasswordHidden.value
? Icons.visibility_off
: Icons.visibility,
color: Colors.blue,
),
onPressed: () =>
controller.toggleConfirmPasswordVisibility(),
),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
validator: controller.validateConfirmPassword,
)),
const SizedBox(height: 15),
// Section heading
const Row(
children: [
Icon(Icons.person_pin_circle, color: Colors.blue),
SizedBox(width: 10),
Text(
'Informasi Profil',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
), ),
), ],
validator: controller.validateConfirmPassword,
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
const Divider(),
const SizedBox(height: 10),
// No HP // No HP
TextFormField( TextFormField(
@ -113,9 +245,22 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nomor HP', labelText: 'Nomor HP',
prefixIcon: const Icon(Icons.phone), hintText: 'Masukkan nomor HP aktif',
border: OutlineInputBorder( prefixIcon: const Icon(Icons.phone, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
), ),
), ),
validator: controller.validateDonaturNoHp, validator: controller.validateDonaturNoHp,
@ -128,10 +273,23 @@ class RegisterDonaturView extends GetView<AuthController> {
keyboardType: TextInputType.streetAddress, keyboardType: TextInputType.streetAddress,
maxLines: 2, maxLines: 2,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Alamat', labelText: 'Alamat Lengkap',
prefixIcon: const Icon(Icons.home), hintText: 'Masukkan alamat lengkap Anda',
border: OutlineInputBorder( prefixIcon: const Icon(Icons.home, color: Colors.blue),
filled: true,
fillColor: Colors.grey.shade100,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
), ),
), ),
validator: controller.validateDonaturAlamat, validator: controller.validateDonaturAlamat,
@ -139,70 +297,160 @@ class RegisterDonaturView extends GetView<AuthController> {
const SizedBox(height: 15), const SizedBox(height: 15),
// Jenis Donatur (Dropdown) // Jenis Donatur (Dropdown)
DropdownButtonFormField<String>( Container(
value: controller.jenisController.text.isEmpty decoration: BoxDecoration(
? 'Individu' color: Colors.grey.shade100,
: controller.jenisController.text, borderRadius: BorderRadius.circular(10),
decoration: InputDecoration( border: Border.all(color: Colors.grey.shade300),
labelText: 'Jenis Donatur', ),
prefixIcon: const Icon(Icons.category), child: DropdownButtonFormField<String>(
border: OutlineInputBorder( value: controller.jenisController.text.isEmpty
borderRadius: BorderRadius.circular(10), ? 'Individu'
), : controller.jenisController.text,
decoration: InputDecoration(
labelText: 'Jenis Donatur',
prefixIcon:
const Icon(Icons.category, color: Colors.blue),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 10),
),
items: const [
DropdownMenuItem(
value: 'Individu', child: Text('Individu')),
DropdownMenuItem(
value: 'Organisasi', child: Text('Organisasi')),
DropdownMenuItem(
value: 'Perusahaan', child: Text('Perusahaan')),
DropdownMenuItem(
value: 'Lainnya', child: Text('Lainnya')),
],
onChanged: (value) {
controller.jenisController.text = value ?? 'Individu';
},
), ),
items: const [
DropdownMenuItem(
value: 'Individu', child: Text('Individu')),
DropdownMenuItem(
value: 'Organisasi', child: Text('Organisasi')),
DropdownMenuItem(
value: 'Perusahaan', child: Text('Perusahaan')),
DropdownMenuItem(
value: 'Lainnya', child: Text('Lainnya')),
],
onChanged: (value) {
controller.jenisController.text = value ?? 'Individu';
},
), ),
const SizedBox(height: 15),
// Register Button const SizedBox(height: 25),
Obx(() => ElevatedButton(
onPressed: controller.isLoading.value // Catatan Informasi
? null Container(
: controller.registerDonatur, padding: const EdgeInsets.all(15),
style: ElevatedButton.styleFrom( decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(vertical: 15), color: Colors.blue.shade50,
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.blue.shade200),
), ),
), child: Row(
child: controller.isLoading.value children: [
? const SpinKitThreeBounce( const Icon(Icons.info_outline, color: Colors.blue),
color: Colors.white, const SizedBox(width: 10),
size: 24, Expanded(
) child: Column(
: const Text( crossAxisAlignment: CrossAxisAlignment.start,
'DAFTAR', children: const [
Text(
'Informasi',
style: TextStyle( style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
SizedBox(height: 5),
Text(
'Data Anda akan terverifikasi dan terlindungi. Kami menjaga privasi dan keamanan data Anda.',
style: TextStyle(
fontSize: 14,
color: Colors.blueGrey,
),
),
],
),
),
],
),
),
const SizedBox(height: 25),
// Register Button
Obx(() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: controller.registerDonatur,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
),
child: controller.isLoading.value
? const SpinKitThreeBounce(
color: Colors.white,
size: 24,
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.how_to_reg, color: Colors.white),
SizedBox(width: 10),
Text(
'DAFTAR SEKARANG',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
)), )),
const SizedBox(height: 20), const SizedBox(height: 20),
// Login Link // Login Link
Row( Container(
mainAxisAlignment: MainAxisAlignment.center, padding: const EdgeInsets.all(15),
children: [ decoration: BoxDecoration(
const Text('Sudah punya akun?'), color: Colors.grey.shade50,
TextButton( borderRadius: BorderRadius.circular(10),
onPressed: () => Get.offAllNamed(Routes.login), border: Border.all(color: Colors.grey.shade200),
child: const Text('Masuk'), ),
), child: Row(
], mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Sudah punya akun?',
style: TextStyle(color: Colors.grey),
),
TextButton(
onPressed: () => Get.offAllNamed(Routes.login),
child: const Text(
'Masuk',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
), ),
const SizedBox(height: 20),
], ],
), ),
), ),

View File

@ -7,6 +7,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart'; import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/lokasi_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart';
@ -45,6 +46,10 @@ class DonaturDashboardController extends GetxController {
// Data untuk stok bantuan yang tersedia // Data untuk stok bantuan yang tersedia
final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs; final RxList<StokBantuanModel> stokBantuan = <StokBantuanModel>[].obs;
// Data untuk lokasi penyaluran
final RxList<LokasiPenyaluranModel> lokasiPenyaluran =
<LokasiPenyaluranModel>[].obs;
// Indikator loading // Indikator loading
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -199,6 +204,9 @@ class DonaturDashboardController extends GetxController {
// Ambil data stok bantuan // Ambil data stok bantuan
await fetchStokBantuan(); await fetchStokBantuan();
// Ambil data lokasi penyaluran
await fetchLokasiPenyaluran();
// Ambil data notifikasi // Ambil data notifikasi
await fetchNotifikasi(); await fetchNotifikasi();
} catch (e) { } catch (e) {
@ -233,7 +241,7 @@ class DonaturDashboardController extends GetxController {
.from('penyaluran_bantuan') .from('penyaluran_bantuan')
.select( .select(
'*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)') '*, lokasi_penyaluran:lokasi_penyaluran_id(*), kategori:kategori_bantuan_id(*), petugas:petugas_id(*)')
.order('tanggal_penyaluran', ascending: true); .order('tanggal_penyaluran', ascending: false);
// Konversi ke model lalu filter di sisi client // Konversi ke model lalu filter di sisi client
final allJadwal = response final allJadwal = response
@ -243,9 +251,7 @@ class DonaturDashboardController extends GetxController {
// Filter jadwal yang tanggalnya lebih besar dari hari ini // Filter jadwal yang tanggalnya lebih besar dari hari ini
jadwalPenyaluran.value = allJadwal jadwalPenyaluran.value = allJadwal
.where((jadwal) => .where((jadwal) => jadwal.tanggalPenyaluran != null)
jadwal.tanggalPenyaluran != null &&
jadwal.tanggalPenyaluran!.isAfter(now))
.toList(); .toList();
} catch (e) { } catch (e) {
print('Error fetching jadwal penyaluran: $e'); print('Error fetching jadwal penyaluran: $e');
@ -306,6 +312,23 @@ class DonaturDashboardController extends GetxController {
} }
} }
// Ambil data lokasi penyaluran
Future<void> fetchLokasiPenyaluran() async {
try {
final response = await _supabaseService.client
.from('lokasi_penyaluran')
.select()
.eq('is_lokasi_titip', true)
.order('nama');
lokasiPenyaluran.value = (response as List<dynamic>)
.map((data) => LokasiPenyaluranModel.fromJson(data))
.toList();
} catch (e) {
print('Error fetching lokasi penyaluran: $e');
}
}
// Ambil data notifikasi // Ambil data notifikasi
Future<void> fetchNotifikasi() async { Future<void> fetchNotifikasi() async {
try { try {
@ -386,6 +409,7 @@ class DonaturDashboardController extends GetxController {
double jumlah, double jumlah,
String deskripsi, String deskripsi,
String? skemaBantuanId, String? skemaBantuanId,
String? lokasiPenyaluranId,
) async { ) async {
try { try {
isLoading.value = true; isLoading.value = true;
@ -426,15 +450,25 @@ class DonaturDashboardController extends GetxController {
'tanggal_penitipan': DateTime.now().toIso8601String(), 'tanggal_penitipan': DateTime.now().toIso8601String(),
'foto_bantuan': fotoBantuanUrls, 'foto_bantuan': fotoBantuanUrls,
'is_uang': selectedStokBantuan.isUang ?? false, 'is_uang': selectedStokBantuan.isUang ?? false,
'skema_bantuan_id': skemaBantuanId,
'lokasi_penyaluran_id': lokasiPenyaluranId,
}; };
// Tambahkan skema bantuan jika ada
if (skemaBantuanId != null && skemaBantuanId.isNotEmpty) {
data['skema_bantuan_id'] = skemaBantuanId;
}
// Simpan ke database // Simpan ke database
await _supabaseService.client.from('penitipan_bantuan').insert(data); final response = await _supabaseService.client
.from('penitipan_bantuan')
.insert(data)
.select('id')
.single();
// Tampilkan pesan sukses
Get.snackbar(
'Berhasil',
'Penitipan bantuan berhasil diinput',
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Reset foto bantuan setelah berhasil disimpan // Reset foto bantuan setelah berhasil disimpan
resetFotoBantuan(); resetFotoBantuan();
@ -442,19 +476,13 @@ class DonaturDashboardController extends GetxController {
// Ambil data penitipan bantuan yang baru // Ambil data penitipan bantuan yang baru
await fetchPenitipanBantuan(); await fetchPenitipanBantuan();
// Tampilkan pesan sukses // Kembali ke halaman utama
Get.snackbar( Get.back();
'Berhasil',
'Penitipan bantuan berhasil dikirim dan akan diproses oleh petugas desa',
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
} catch (e) { } catch (e) {
print('Error creating penitipan bantuan: $e'); print('Error creating penitipan bantuan: $e');
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat mengirim penitipan bantuan: $e', 'Terjadi kesalahan: $e',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
@ -475,7 +503,7 @@ class DonaturDashboardController extends GetxController {
.eq('id', lokasiId) .eq('id', lokasiId)
.single(); .single();
if (response != null && response['nama'] != null) { if (response['nama'] != null) {
return response['nama'] as String; return response['nama'] as String;
} }
return null; return null;

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart';
class DonaturDashboardView extends GetView<DonaturDashboardController> { class DonaturDashboardView extends GetView<DonaturDashboardController> {
@ -36,13 +36,57 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header DisalurKita dengan logo dan slogan
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
_buildWelcomeSection(), _buildWelcomeSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildStatisticSection(), _buildStatisticSection(),
const SizedBox(height: 24),
_buildUpcomingEvents(),
const SizedBox(height: 24),
_buildRecentPenitipan(),
], ],
), ),
), ),
@ -101,14 +145,24 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: CircleAvatar( child: CircleAvatar(
radius: 30, radius: 30,
backgroundColor: Colors.blue.shade100, backgroundColor: Colors.blue.shade100,
backgroundImage: controller.profilePhotoUrl != null backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!) ? NetworkImage(controller.profilePhotoUrl!)
: null, : null,
child: controller.profilePhotoUrl == null child: (controller.profilePhotoUrl == null ||
? Icon( controller.profilePhotoUrl!.isEmpty)
Icons.person, ? Text(
color: Colors.blue.shade700, controller.nama.isNotEmpty
size: 30, ? controller.nama
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
) )
: null, : null,
), ),
@ -263,7 +317,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
child: _buildStatCard( child: _buildStatCard(
title: 'Diterima', title: 'Diterima',
value: value:
'${controller.penitipanBantuan.where((p) => p.status == 'DITERIMA').length}', '${controller.penitipanBantuan.where((p) => p.status == 'TERVERIFIKASI').length}',
icon: Icons.check_circle_outline, icon: Icons.check_circle_outline,
color: Colors.green, color: Colors.green,
), ),
@ -284,125 +338,6 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
); );
} }
Widget _buildUpcomingEvents() {
final upcomingEvents = controller.jadwalPenyaluran
.where((event) =>
event.tanggalPenyaluran != null &&
event.tanggalPenyaluran!.isAfter(DateTime.now()))
.take(3)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Jadwal Penyaluran',
),
Text(
'Jadwal penyaluran bantuan terdekat',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
TextButton(
onPressed: () {
// Navigasi ke tab jadwal penyaluran
controller.activeTabIndex.value = 2;
},
child: Text(
'Lihat Semua',
style: TextStyle(color: Colors.blue.shade700),
),
),
],
),
const SizedBox(height: 8),
if (upcomingEvents.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: const Center(
child: Text(
'Tidak ada jadwal penyaluran dalam waktu dekat',
style: TextStyle(color: Colors.grey),
),
),
)
else
...upcomingEvents.map((event) => _buildEventCard(event)),
],
);
}
Widget _buildRecentPenitipan() {
final recentPenitipan = controller.penitipanBantuan.take(3).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(
title: 'Bantuan Terakhir',
),
Text(
'Riwayat penitipan bantuan terakhir',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
TextButton(
onPressed: () {
// Navigasi ke tab riwayat penitipan
controller.activeTabIndex.value = 3;
},
child: Text(
'Lihat Semua',
style: TextStyle(color: Colors.blue.shade700),
),
),
],
),
const SizedBox(height: 8),
if (recentPenitipan.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: const Center(
child: Text(
'Belum ada riwayat penitipan bantuan',
style: TextStyle(color: Colors.grey),
),
),
)
else
...recentPenitipan.map((penitipan) => _buildPenitipanCard(penitipan)),
],
);
}
Widget _buildInfoRow({ Widget _buildInfoRow({
required IconData icon, required IconData icon,
required Color iconColor, required Color iconColor,
@ -545,7 +480,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
Widget _buildEventCard(dynamic event) { Widget _buildEventCard(dynamic event) {
final formattedDate = event.tanggalPenyaluran != null final formattedDate = event.tanggalPenyaluran != null
? DateFormat('dd MMMM yyyy', 'id_ID').format(event.tanggalPenyaluran!) ? FormatHelper.formatDateTime(event.tanggalPenyaluran!)
: 'Tanggal tidak tersedia'; : 'Tanggal tidak tersedia';
return Container( return Container(
@ -588,7 +523,8 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
), ),
Text( Text(
event.tanggalPenyaluran != null event.tanggalPenyaluran != null
? DateFormat('dd').format(event.tanggalPenyaluran!) ? FormatHelper.formatDateTime(
event.tanggalPenyaluran!)
: '--', : '--',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@ -640,8 +576,7 @@ class DonaturDashboardView extends GetView<DonaturDashboardController> {
Widget _buildPenitipanCard(dynamic penitipan) { Widget _buildPenitipanCard(dynamic penitipan) {
final formattedDate = penitipan.tanggalPenitipan != null final formattedDate = penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
.format(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia'; : 'Tanggal tidak tersedia';
Color statusColor; Color statusColor;

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
class DonaturJadwalDetailView extends GetView<DonaturDashboardController> { class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
const DonaturJadwalDetailView({Key? key}) : super(key: key); const DonaturJadwalDetailView({super.key});
@override @override
DonaturDashboardController get controller { DonaturDashboardController get controller {
@ -35,7 +35,6 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
_buildDetailSection(jadwal), _buildDetailSection(jadwal),
_buildPelaksanaSection(jadwal), _buildPelaksanaSection(jadwal),
_buildStatusSection(jadwal), _buildStatusSection(jadwal),
_buildActionSection(jadwal),
], ],
), ),
), ),
@ -126,8 +125,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
DateFormat('EEEE, dd MMMM yyyy', 'id_ID') FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
.format(jadwal.tanggalPenyaluran!),
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.white, color: Colors.white,
@ -204,11 +202,26 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
CircleAvatar( CircleAvatar(
radius: 25, radius: 25,
backgroundColor: Colors.blue.shade100, backgroundColor: Colors.blue.shade100,
child: Icon( backgroundImage: jadwal.fotoPetugas != null &&
Icons.person, jadwal.fotoPetugas.toString().isNotEmpty
color: Colors.blue.shade700, ? NetworkImage(jadwal.fotoPetugas as String)
size: 30, : null,
), child: (jadwal.fotoPetugas == null ||
jadwal.fotoPetugas.toString().isEmpty)
? Text(
jadwal.namaPetugas != null
? jadwal.namaPetugas
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 20,
),
)
: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@ -254,50 +267,87 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
children: [ children: [
const SectionHeader(title: 'Status Penyaluran'), const SectionHeader(title: 'Status Penyaluran'),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildStatusTimeline(jadwal), _buildStatusCard(jadwal),
], ],
), ),
); );
} }
Widget _buildStatusTimeline(PenyaluranBantuanModel jadwal) { Widget _buildStatusCard(PenyaluranBantuanModel jadwal) {
final status = jadwal.status; final status = jadwal.status;
final bool isCompleted = status == 'SELESAI'; final bool isCompleted = status == 'TERLAKSANA';
final bool isCancelled = status == 'DIBATALKAN'; final bool isCancelled = status == 'BATALTERLAKSANA';
final bool isInProgress = status == 'DALAM_PROSES'; final bool isInProgress = status == 'AKTIF';
final bool isScheduled = status == 'Dijadwalkan';
Color statusColor = Colors.blue;
IconData statusIcon = Icons.schedule;
String statusText = 'Dijadwalkan';
if (isCompleted) {
statusColor = Colors.green;
statusIcon = Icons.check_circle;
statusText = 'Terlaksana';
} else if (isCancelled) {
statusColor = Colors.red;
statusIcon = Icons.cancel;
statusText = 'Batal Terlaksana';
} else if (isInProgress) {
statusColor = Colors.blue;
statusIcon = Icons.sync;
statusText = 'Aktif';
} else if (isScheduled) {
statusColor = Colors.orange;
statusIcon = Icons.schedule;
statusText = 'Dijadwalkan';
}
return Column( return Column(
children: [ children: [
_buildTimelineItem( Container(
title: 'Dijadwalkan', padding: const EdgeInsets.all(16),
date: jadwal.createdAt != null decoration: BoxDecoration(
? DateFormat('dd MMM yyyy', 'id_ID').format(jadwal.createdAt!) color: statusColor.withOpacity(0.1),
: '-', borderRadius: BorderRadius.circular(12),
isCompleted: true, border: Border.all(color: statusColor.withOpacity(0.3)),
isFirst: true, ),
), child: Column(
_buildTimelineItem( children: [
title: 'Dalam Proses', Row(
date: isInProgress || isCompleted children: [
? jadwal.tanggalPenyaluran != null Icon(statusIcon, color: statusColor, size: 28),
? DateFormat('dd MMM yyyy', 'id_ID') const SizedBox(width: 12),
.format(jadwal.tanggalPenyaluran!) Text(
: '-' statusText,
: '-', style: TextStyle(
isCompleted: isInProgress || isCompleted, fontSize: 18,
isCancelled: isCancelled, fontWeight: FontWeight.bold,
), color: statusColor,
_buildTimelineItem( ),
title: 'Selesai', ),
date: isCompleted ],
? jadwal.tanggalSelesai != null ),
? DateFormat('dd MMM yyyy', 'id_ID') const SizedBox(height: 16),
.format(jadwal.tanggalSelesai!) _buildStatusDetailItem(
: '-' title: 'Tanggal Dijadwalkan',
: '-', value: FormatHelper.formatDateIndonesian(jadwal.createdAt),
isCompleted: isCompleted, ),
isCancelled: isCancelled, const SizedBox(height: 8),
isLast: true, _buildStatusDetailItem(
title: 'Tanggal Penyaluran',
value:
FormatHelper.formatDateIndonesian(jadwal.tanggalPenyaluran),
),
if (isCompleted) ...[
const SizedBox(height: 8),
_buildStatusDetailItem(
title: 'Tanggal Selesai',
value:
FormatHelper.formatDateIndonesian(jadwal.tanggalSelesai),
),
],
],
),
), ),
if (isCancelled) ...[ if (isCancelled) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
@ -333,7 +383,7 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
const SizedBox(height: 8), const SizedBox(height: 8),
if (jadwal.tanggalPembatalan != null) if (jadwal.tanggalPembatalan != null)
Text( Text(
'Dibatalkan pada: ${DateFormat('dd MMMM yyyy', 'id_ID').format(jadwal.tanggalPembatalan!)}', 'Dibatalkan pada: ${FormatHelper.formatDateIndonesian(jadwal.tanggalPembatalan)}',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.red.shade700, color: Colors.red.shade700,
@ -347,159 +397,29 @@ class DonaturJadwalDetailView extends GetView<DonaturDashboardController> {
); );
} }
Widget _buildTimelineItem({ Widget _buildStatusDetailItem(
required String title, {required String title, required String value}) {
required String date,
required bool isCompleted,
bool isFirst = false,
bool isLast = false,
bool isCancelled = false,
}) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( Text(
width: 20, title,
child: Column( style: TextStyle(
children: [ fontSize: 14,
if (!isFirst) color: Colors.grey.shade700,
Container(
width: 2,
height: 20,
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
border: Border.all(
color: isCompleted
? Colors.green
: isCancelled
? Colors.red
: Colors.grey.shade300,
width: 2,
),
),
child: isCompleted
? const Icon(Icons.check, size: 12, color: Colors.white)
: isCancelled
? const Icon(Icons.close, size: 12, color: Colors.white)
: null,
),
if (!isLast)
Container(
width: 2,
height: 20,
color: isCompleted && !isCancelled
? Colors.green
: Colors.grey.shade300,
),
],
), ),
), ),
const SizedBox(width: 12), Text(
Expanded( value,
child: Container( style: const TextStyle(
margin: const EdgeInsets.only(bottom: 16), fontSize: 14,
child: Column( fontWeight: FontWeight.bold,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isCompleted
? Colors.black
: isCancelled
? Colors.red
: Colors.grey,
),
),
Text(
date,
style: TextStyle(
fontSize: 14,
color: isCompleted
? Colors.grey.shade700
: isCancelled
? Colors.red.shade300
: Colors.grey.shade400,
),
),
],
),
), ),
), ),
], ],
); );
} }
Widget _buildActionSection(PenyaluranBantuanModel jadwal) {
if (jadwal.status == 'DIBATALKAN') {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader(title: 'Tindakan'),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _hubungiPetugas(jadwal),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.chat_outlined),
label: const Text('Hubungi Petugas'),
),
),
if (jadwal.status == 'SELESAI') ...[
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _lihatLaporan(jadwal),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
foregroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.description_outlined),
label: const Text('Lihat Laporan'),
),
),
],
],
),
],
),
);
}
Widget _buildInfoItem({ Widget _buildInfoItem({
required IconData icon, required IconData icon,
required String title, required String title,

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart';
class DonaturJadwalView extends GetView<DonaturDashboardController> { class DonaturJadwalView extends GetView<DonaturDashboardController> {
@ -97,7 +98,7 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
for (var jadwal in controller.jadwalPenyaluran) { for (var jadwal in controller.jadwalPenyaluran) {
if (jadwal.tanggalPenyaluran != null) { if (jadwal.tanggalPenyaluran != null) {
String monthYear = String monthYear =
DateFormat('MMMM yyyy', 'id_ID').format(jadwal.tanggalPenyaluran!); FormatHelper.formatDate(jadwal.tanggalPenyaluran!, format: 'MMMM');
if (!groupedJadwal.containsKey(monthYear)) { if (!groupedJadwal.containsKey(monthYear)) {
groupedJadwal[monthYear] = []; groupedJadwal[monthYear] = [];
@ -110,9 +111,14 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
// Urutkan kunci (bulan) secara kronologis // Urutkan kunci (bulan) secara kronologis
List<String> sortedMonths = groupedJadwal.keys.toList() List<String> sortedMonths = groupedJadwal.keys.toList()
..sort((a, b) { ..sort((a, b) {
DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a); try {
DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b); DateTime dateA = DateFormat('MMMM yyyy', 'id_ID').parse(a);
return dateA.compareTo(dateB); DateTime dateB = DateFormat('MMMM yyyy', 'id_ID').parse(b);
return dateA.compareTo(dateB);
} catch (e) {
// Fallback sorting jika parse error
return a.compareTo(b);
}
}); });
return ListView( return ListView(
@ -158,28 +164,27 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
Widget _buildJadwalCard(dynamic jadwal) { Widget _buildJadwalCard(dynamic jadwal) {
final formattedDate = jadwal.tanggalPenyaluran != null final formattedDate = jadwal.tanggalPenyaluran != null
? DateFormat('EEEE, dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(jadwal.tanggalPenyaluran!)
.format(jadwal.tanggalPenyaluran!)
: 'Tanggal tidak tersedia'; : 'Tanggal tidak tersedia';
String statusText = 'Akan Datang'; String statusText = 'Dijadwalkan';
Color statusColor = Colors.blue; Color statusColor = Colors.blue;
switch (jadwal.status) { switch (jadwal.status) {
case 'SELESAI': case 'TERLAKSANA':
statusText = 'Selesai'; statusText = 'Terlaksana';
statusColor = Colors.green; statusColor = Colors.green;
break; break;
case 'DIBATALKAN': case 'BATALTERLAKSANA':
statusText = 'Dibatalkan'; statusText = 'Batal Terlaksana';
statusColor = Colors.red; statusColor = Colors.red;
break; break;
case 'DALAM_PROSES': case 'AKTIF':
statusText = 'Dalam Proses'; statusText = 'Aktif';
statusColor = Colors.orange; statusColor = Colors.blue;
break; break;
default: default:
statusText = 'Akan Datang'; statusText = 'Dijadwalkan';
statusColor = Colors.blue; statusColor = Colors.blue;
} }
@ -248,8 +253,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
), ),
child: Text( child: Text(
jadwal.tanggalPenyaluran != null jadwal.tanggalPenyaluran != null
? DateFormat('MMM', 'id_ID') ? FormatHelper.formatDate(
.format(jadwal.tanggalPenyaluran!) jadwal.tanggalPenyaluran!,
format: 'MMM')
.toUpperCase() .toUpperCase()
: 'TBD', : 'TBD',
style: const TextStyle( style: const TextStyle(
@ -265,8 +271,9 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
child: Center( child: Center(
child: Text( child: Text(
jadwal.tanggalPenyaluran != null jadwal.tanggalPenyaluran != null
? DateFormat('dd') ? FormatHelper.formatDate(
.format(jadwal.tanggalPenyaluran!) jadwal.tanggalPenyaluran!,
format: 'dd')
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
@ -459,11 +466,11 @@ class DonaturJadwalView extends GetView<DonaturDashboardController> {
IconData _getStatusIcon(String? status) { IconData _getStatusIcon(String? status) {
switch (status) { switch (status) {
case 'SELESAI': case 'TERLAKSANA':
return Icons.check_circle; return Icons.check_circle;
case 'DIBATALKAN': case 'BATALTERLAKSANA':
return Icons.cancel; return Icons.cancel;
case 'DALAM_PROSES': case 'AKTIF':
return Icons.timelapse; return Icons.timelapse;
default: default:
return Icons.event_available; return Icons.event_available;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> { class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
DonaturRiwayatPenitipanView({super.key}); DonaturRiwayatPenitipanView({super.key});
@ -60,8 +61,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? ''; final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? ''; final deskripsi = item.deskripsi?.toLowerCase() ?? '';
final tanggal = item.tanggalPenitipan != null final tanggal = item.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(item.tanggalPenitipan!)
.format(item.tanggalPenitipan!)
.toLowerCase() .toLowerCase()
: ''; : '';
@ -214,8 +214,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
Widget _buildPenitipanCard( Widget _buildPenitipanCard(
BuildContext context, dynamic penitipan, Color statusColor) { BuildContext context, dynamic penitipan, Color statusColor) {
final formattedDate = penitipan.tanggalPenitipan != null final formattedDate = penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
.format(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia'; : 'Tanggal tidak tersedia';
IconData statusIcon; IconData statusIcon;
@ -435,61 +434,6 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
return id != null ? 'Petugas Desa' : 'Tidak ada petugas'; return id != null ? 'Petugas Desa' : 'Tidak ada petugas';
} }
void showFullScreenImage(String imageUrl) {
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
),
],
),
),
),
);
}
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('Detail Penitipan'), title: const Text('Detail Penitipan'),
@ -509,8 +453,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
_buildInfoRow( _buildInfoRow(
'Tanggal Penitipan', 'Tanggal Penitipan',
penitipan.tanggalPenitipan != null penitipan.tanggalPenitipan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(penitipan.tanggalPenitipan!)
.format(penitipan.tanggalPenitipan!)
: 'Tanggal tidak tersedia', : 'Tanggal tidak tersedia',
), ),
_buildInfoRow( _buildInfoRow(
@ -520,8 +463,7 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
if (penitipan.tanggalVerifikasi != null) if (penitipan.tanggalVerifikasi != null)
_buildInfoRow( _buildInfoRow(
'Tanggal Verifikasi', 'Tanggal Verifikasi',
DateFormat('dd MMMM yyyy HH:mm', 'id_ID') FormatHelper.formatDateTime(penitipan.tanggalVerifikasi!),
.format(penitipan.tanggalVerifikasi!),
), ),
if (penitipan.deskripsi != null && if (penitipan.deskripsi != null &&
penitipan.deskripsi!.isNotEmpty) penitipan.deskripsi!.isNotEmpty)
@ -543,8 +485,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => onTap: () => ShowImageDialog.showFullScreen(
showFullScreenImage(penitipan.fotoBantuan!.first), context,
penitipan.fotoBantuan!.first,
),
child: Container( child: Container(
height: 200, height: 200,
width: double.infinity, width: double.infinity,
@ -572,8 +516,10 @@ class DonaturRiwayatPenitipanView extends GetView<DonaturDashboardController> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => onTap: () => ShowImageDialog.showFullScreen(
showFullScreenImage(penitipan.fotoBuktiSerahTerima!), context,
penitipan.fotoBuktiSerahTerima!,
),
child: Container( child: Container(
height: 200, height: 200,
width: double.infinity, width: double.infinity,

View File

@ -4,7 +4,6 @@ import 'package:penyaluran_app/app/modules/donatur/controllers/donatur_dashboard
import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/utils/date_helper.dart';
class DonaturSkemaView extends GetView<DonaturDashboardController> { class DonaturSkemaView extends GetView<DonaturDashboardController> {
const DonaturSkemaView({super.key}); const DonaturSkemaView({super.key});
@ -549,14 +548,14 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
int days = difference.inDays; int days = difference.inDays;
if (days > 0) { if (days > 0) {
return 'Batas waktu: ${days} hari lagi'; return 'Batas waktu: $days hari lagi';
} else { } else {
int hours = difference.inHours; int hours = difference.inHours;
if (hours > 0) { if (hours > 0) {
return 'Batas waktu: ${hours} jam lagi'; return 'Batas waktu: $hours jam lagi';
} else { } else {
int minutes = difference.inMinutes; int minutes = difference.inMinutes;
return 'Batas waktu: ${minutes} menit lagi'; return 'Batas waktu: $minutes menit lagi';
} }
} }
} }
@ -597,20 +596,20 @@ class DonaturSkemaView extends GetView<DonaturDashboardController> {
} }
} }
// Format nilai sebagai Rupiah menggunakan DateHelper // Format nilai sebagai Rupiah menggunakan DateHelper
return DateHelper.formatRupiah(nilai); return FormatHelper.formatRupiah(nilai);
} }
// Jika bukan uang, kembalikan nilai + satuan (jika ada) // Jika bukan uang, kembalikan nilai + satuan (jika ada)
return '${jumlahDiterimaPerOrang} ${stokBantuan.satuan ?? ''}'; return '$jumlahDiterimaPerOrang ${stokBantuan.satuan ?? ''}';
} }
String _formatRupiah(dynamic amount) { String _formatRupiah(dynamic amount) {
if (amount is num) { if (amount is num) {
return DateHelper.formatRupiah(amount); return FormatHelper.formatRupiah(amount);
} else if (amount is String) { } else if (amount is String) {
try { try {
double nilai = double.parse(amount); double nilai = double.parse(amount);
return DateHelper.formatRupiah(nilai); return FormatHelper.formatRupiah(nilai);
} catch (e) { } catch (e) {
return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}'; return 'Rp ${amount.replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
} }

View File

@ -34,7 +34,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
title: Obx(() { title: Obx(() {
switch (controller.activeTabIndex.value) { switch (controller.activeTabIndex.value) {
case 0: case 0:
return const Text('Dashboard Donatur'); return const Text('Dashboard');
case 1: case 1:
return const Text('Skema Bantuan'); return const Text('Skema Bantuan');
case 2: case 2:
@ -44,7 +44,7 @@ class DonaturView extends GetView<DonaturDashboardController> {
case 4: case 4:
return const Text('Laporan Penyaluran'); return const Text('Laporan Penyaluran');
default: default:
return const Text('Dashboard Donatur'); return const Text('Dashboard');
} }
}), }),
leading: IconButton( leading: IconButton(
@ -201,12 +201,20 @@ class DonaturView extends GetView<DonaturDashboardController> {
controller.profilePhotoUrl!.isNotEmpty controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!) ? NetworkImage(controller.profilePhotoUrl!)
: null, : null,
child: controller.profilePhotoUrl == null || child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty controller.profilePhotoUrl!.isEmpty)
? const Icon( ? Text(
Icons.person, controller.nama.isNotEmpty
color: Colors.white, ? controller.nama
size: 40, .toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
) )
: null, : null,
), ),
@ -284,44 +292,175 @@ class DonaturView extends GetView<DonaturDashboardController> {
child: ListView( child: ListView(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: [ children: [
ListTile( _buildMenuCategory('Menu Utama'),
leading: const Icon(Icons.person_outline), Obx(() => _buildMenuItem(
title: const Text('Profil'), icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
title: 'Dashboard',
isSelected: controller.activeTabIndex.value == 0,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 0;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.description_outlined,
activeIcon: Icons.description,
title: 'Skema Bantuan',
isSelected: controller.activeTabIndex.value == 1,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 1;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.calendar_today_outlined,
activeIcon: Icons.calendar_today,
title: 'Jadwal Penyaluran',
isSelected: controller.activeTabIndex.value == 2,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 2;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.add_box_outlined,
activeIcon: Icons.add_box,
title: 'Penitipan Bantuan',
isSelected: controller.activeTabIndex.value == 3,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 3;
},
)),
Obx(() => _buildMenuItem(
icon: Icons.assignment_outlined,
activeIcon: Icons.assignment,
title: 'Laporan Penyaluran',
isSelected: controller.activeTabIndex.value == 4,
onTap: () {
Navigator.pop(context);
controller.activeTabIndex.value = 4;
},
)),
_buildMenuCategory('Pengaturan'),
_buildMenuItem(
icon: Icons.person_outline,
activeIcon: Icons.person,
title: 'Profil',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/profile'); Get.toNamed('/profile');
}, },
), ),
ListTile( _buildMenuItem(
leading: const Icon(Icons.history), icon: Icons.info_outline,
title: const Text('Riwayat Donasi'), activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
// TODO: Implementasi riwayat donasi Get.toNamed('/about');
}, },
), ),
ListTile( _buildMenuItem(
leading: const Icon(Icons.settings_outlined), icon: Icons.logout,
title: const Text('Pengaturan'), title: 'Keluar',
onTap: () {
Navigator.pop(context);
// TODO: Implementasi pengaturan
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Keluar'),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
controller.logout(); controller.logout();
}, },
isLogout: true,
), ),
], ],
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
], ],
), ),
); );
} }
Widget _buildMenuCategory(String title) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
);
}
Widget _buildMenuItem({
required IconData icon,
IconData? activeIcon,
required String title,
bool isSelected = false,
String? badge,
required Function() onTap,
bool isLogout = false,
}) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryColor.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: ListTile(
leading: Icon(
isSelected ? (activeIcon ?? icon) : icon,
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
),
title: Text(
title,
style: TextStyle(
color: isSelected
? AppTheme.primaryColor
: (isLogout ? Colors.red : null),
fontWeight: isSelected ? FontWeight.bold : null,
),
),
trailing: badge != null
? Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
badge,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
)
: null,
onTap: onTap,
),
);
}
} }

View File

@ -11,7 +11,6 @@ import 'package:path_provider/path_provider.dart';
import 'dart:io'; import 'dart:io';
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
@ -635,7 +634,7 @@ class LaporanPenyaluranController extends GetxController {
fontSize: 12, fontSize: 12,
color: PdfColors.blue900)), color: PdfColors.blue900)),
pw.Text( pw.Text(
'Tanggal: ${DateFormat('dd MMMM yyyy').format(DateTime.now())}', 'Tanggal: ${FormatHelper.formatDateTime(DateTime.now())}',
style: pw.TextStyle(font: ttf, fontSize: 10), style: pw.TextStyle(font: ttf, fontSize: 10),
), ),
], ],
@ -708,8 +707,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow( _buildPdfRow(
'Tanggal Laporan', 'Tanggal Laporan',
laporan.tanggalLaporan != null laporan.tanggalLaporan != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(laporan.tanggalLaporan!)
laporan.tanggalLaporan!)
: '-', : '-',
ttf, ttf,
ttfBold), ttfBold),
@ -731,7 +729,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow( _buildPdfRow(
'Tanggal Penyaluran', 'Tanggal Penyaluran',
penyaluran.tanggalPenyaluran != null penyaluran.tanggalPenyaluran != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
penyaluran.tanggalPenyaluran!) penyaluran.tanggalPenyaluran!)
: '-', : '-',
ttf, ttf,
@ -739,7 +737,7 @@ class LaporanPenyaluranController extends GetxController {
_buildPdfRow( _buildPdfRow(
'Tanggal Selesai', 'Tanggal Selesai',
penyaluran.tanggalSelesai != null penyaluran.tanggalSelesai != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!) penyaluran.tanggalSelesai!)
: '-', : '-',
ttf, ttf,
@ -902,7 +900,7 @@ class LaporanPenyaluranController extends GetxController {
final isUang = stokBantuan['is_uang'] == true; final isUang = stokBantuan['is_uang'] == true;
final formattedJumlah = isUang final formattedJumlah = isUang
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlah)}' ? FormatHelper.formatRupiah(jumlah)
: '$jumlah ${stokBantuan['satuan'] ?? ''}'; : '$jumlah ${stokBantuan['satuan'] ?? ''}';
return pw.TableRow( return pw.TableRow(
@ -975,7 +973,7 @@ class LaporanPenyaluranController extends GetxController {
final jumlahBantuan = penerima.jumlahBantuan ?? 0; final jumlahBantuan = penerima.jumlahBantuan ?? 0;
final formattedJumlah = isUang final formattedJumlah = isUang
? 'Rp ${NumberFormat.currency(locale: 'id', symbol: '', decimalDigits: 0).format(jumlahBantuan)}' ? FormatHelper.formatRupiah(jumlahBantuan)
: '$jumlahBantuan ${penerima.satuan ?? ''}'; : '$jumlahBantuan ${penerima.satuan ?? ''}';
return pw.TableRow( return pw.TableRow(

View File

@ -65,7 +65,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
controller.selectedPenyaluran.value! controller.selectedPenyaluran.value!
.tanggalPenyaluran != .tanggalPenyaluran !=
null null
? DateTimeHelper.formatDateTime(controller ? FormatHelper.formatDateTime(controller
.selectedPenyaluran.value!.tanggalPenyaluran!) .selectedPenyaluran.value!.tanggalPenyaluran!)
: '-', : '-',
), ),
@ -73,7 +73,7 @@ class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
'Tanggal Selesai', 'Tanggal Selesai',
controller.selectedPenyaluran.value!.tanggalSelesai != controller.selectedPenyaluran.value!.tanggalSelesai !=
null null
? DateTimeHelper.formatDateTime(controller ? FormatHelper.formatDateTime(controller
.selectedPenyaluran.value!.tanggalSelesai!) .selectedPenyaluran.value!.tanggalSelesai!)
: '-', : '-',
), ),

View File

@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart'; import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart';
import 'package:intl/intl.dart';
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> { class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
const LaporanPenyaluranView({super.key}); const LaporanPenyaluranView({super.key});
@ -255,8 +254,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 8), // const SizedBox(width: 8),
StatusBadge(status: laporan.status ?? 'DRAFT'), // StatusBadge(status: laporan.status ?? 'DRAFT'),
], ],
), ),
), ),
@ -273,10 +272,11 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
Icons.calendar_today, Icons.calendar_today,
'Tanggal', 'Tanggal',
laporan.tanggalLaporan != null laporan.tanggalLaporan != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
laporan.tanggalLaporan!) laporan.tanggalLaporan!)
: '-', : '-',
), ),
const SizedBox(width: 16),
_buildInfoItem( _buildInfoItem(
Icons.description, Icons.description,
'Status', 'Status',
@ -538,8 +538,8 @@ class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
penyaluran.tanggalSelesai != null penyaluran.tanggalSelesai != null
? DateFormat('dd/MM/yyyy') ? FormatHelper.formatDateTime(
.format(penyaluran.tanggalSelesai!) penyaluran.tanggalSelesai!)
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,

View File

@ -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
);
}
}
}

View File

@ -298,7 +298,7 @@ class CalendarViewWidget extends StatelessWidget {
for (var jadwal in allJadwal) { for (var jadwal in allJadwal) {
if (jadwal.tanggalPenyaluran != null) { if (jadwal.tanggalPenyaluran != null) {
DateTime jadwalDate = DateTime jadwalDate =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!); FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
if (jadwalDate if (jadwalDate
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) && .isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
@ -346,7 +346,7 @@ class CalendarViewWidget extends StatelessWidget {
void _showAppointmentDetails(BuildContext context, Appointment appointment) { void _showAppointmentDetails(BuildContext context, Appointment appointment) {
final String formattedDate = final String formattedDate =
DateTimeHelper.formatDateIndonesian(appointment.startTime); FormatHelper.formatDateIndonesian(appointment.startTime);
// Dapatkan status dari ID jadwal // Dapatkan status dari ID jadwal
String? status = _getStatusFromAppointmentId(appointment.id); String? status = _getStatusFromAppointmentId(appointment.id);

View File

@ -207,7 +207,7 @@ class JadwalSectionWidget extends StatelessWidget {
// Format tanggal dan waktu menggunakan helper // Format tanggal dan waktu menggunakan helper
String formattedDateTime = String formattedDateTime =
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran); FormatHelper.formatDateTime(jadwal.tanggalPenyaluran);
// Dapatkan nama lokasi dan kategori // Dapatkan nama lokasi dan kategori
String lokasiName = String lokasiName =

View File

@ -211,18 +211,16 @@ class DetailPenyaluranController extends GetxController {
.eq('id', penerima.id!) .eq('id', penerima.id!)
.single(); .single();
if (penerimaData != null) { final String stokBantuanId = penerimaData['stok_bantuan_id'];
final String stokBantuanId = penerimaData['stok_bantuan_id']; final double jumlah = penerimaData['jumlah_bantuan'] is int
final double jumlah = penerimaData['jumlah_bantuan'] is int ? penerimaData['jumlah_bantuan'].toDouble()
? penerimaData['jumlah_bantuan'].toDouble() : penerimaData['jumlah_bantuan'];
: penerimaData['jumlah_bantuan'];
// Kurangi stok dan catat riwayat // Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id; final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) { if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran( await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId); penerima.id!, stokBantuanId, jumlah, petugasId);
}
} }
// Refresh data setelah konfirmasi berhasil // Refresh data setelah konfirmasi berhasil

View File

@ -11,14 +11,21 @@ import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'package:penyaluran_app/app/services/notification_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
class JadwalPenyaluranController extends GetxController { class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>(); final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to; final SupabaseService _supabaseService = SupabaseService.to;
late final JadwalUpdateService _jadwalUpdateService;
late final StreamSubscription _jadwalUpdateSubscription;
SupabaseService get supabaseService => _supabaseService; SupabaseService get supabaseService => _supabaseService;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxBool isLoadingStatusUpdate = false.obs;
final RxBool isLokasiLoading = false.obs;
// Indeks kategori yang dipilih untuk filter // Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs; final RxInt selectedCategoryIndex = 0.obs;
@ -52,6 +59,21 @@ class JadwalPenyaluranController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Inisialisasi JadwalUpdateService
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('JadwalPenyaluranController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadJadwalData(); loadJadwalData();
loadPermintaanPenjadwalanData(); loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData(); loadLokasiPenyaluranData();
@ -67,100 +89,444 @@ class JadwalPenyaluranController extends GetxController {
searchController.dispose(); searchController.dispose();
// Hentikan timer jika ada // Hentikan timer jika ada
_stopJadwalCheckTimer(); _stopJadwalCheckTimer();
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService.unregisterFromUpdates('JadwalPenyaluranController');
super.onClose(); super.onClose();
} }
// Timer untuk memeriksa jadwal secara berkala // Timer untuk memeriksa jadwal secara berkala
Timer? _jadwalCheckTimer; Timer? _jadwalCheckTimer;
Timer?
_intensiveCheckTimer; // Timer untuk pengecekan intensif mendekati waktu penyaluran
final RxBool _intensiveCheckActive = false.obs; // Status pengecekan intensif
void _startJadwalCheckTimer() { void _startJadwalCheckTimer() {
// Periksa jadwal setiap 1 menit // Dengan fitur realtime yang sudah aktif, kita bisa mengurangi frekuensi polling
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) { // Cek setiap 30 detik sebagai fallback untuk realtime
checkAndUpdateJadwalStatus(); _jadwalCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
}); });
// Periksa jadwal segera saat aplikasi dimulai // Periksa jadwal segera saat aplikasi dimulai
checkAndUpdateJadwalStatus(); checkAndUpdateJadwalStatus();
// Log info untuk debugging
print('Jadwal check timer started with 30 seconds interval');
// Mulai juga pengecekan jadwal yang akan datang
_startUpcomingJadwalCheck();
} }
void _stopJadwalCheckTimer() { void _stopJadwalCheckTimer() {
_jadwalCheckTimer?.cancel(); _jadwalCheckTimer?.cancel();
_jadwalCheckTimer = null; _jadwalCheckTimer = null;
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
}
// Metode baru untuk memeriksa jadwal mendatang dan memulai pemeriksaan intensif jika perlu
void _startUpcomingJadwalCheck() {
Timer.periodic(const Duration(minutes: 1), (timer) {
// Jika sudah ada timer intensif yang berjalan, tidak perlu melakukan pengecekan lagi
if (_intensiveCheckActive.value) return;
final now = DateTime.now();
bool foundUpcomingJadwal = false;
// Periksa apakah ada jadwal yang akan aktif dalam 10 menit ke depan
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika ada jadwal dalam 10 menit ke depan, mulai pemeriksaan intensif
if (diff >= 0 && diff <= 10) {
print(
'Found upcoming jadwal in $diff minutes: ${jadwal.id} - ${jadwal.nama}');
foundUpcomingJadwal = true;
break;
}
}
}
// Jika ditemukan jadwal yang akan datang, mulai pemeriksaan intensif
if (foundUpcomingJadwal && !_intensiveCheckActive.value) {
_startIntensiveCheck();
}
});
}
// Metode untuk memulai pemeriksaan intensif untuk jadwal yang mendekati waktu
void _startIntensiveCheck() {
if (_intensiveCheckActive.value) return;
_intensiveCheckActive.value = true;
print('Starting intensive jadwal check every 5 seconds');
// Periksa setiap 5 detik
_intensiveCheckTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
// Periksa apakah masih perlu melakukan pemeriksaan intensif
final now = DateTime.now();
bool needIntensiveCheck = false;
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika masih ada jadwal dalam 10 menit ke depan, lanjutkan pemeriksaan
if (diff >= -5 && diff <= 10) {
needIntensiveCheck = true;
break;
}
}
}
// Jika tidak ada lagi jadwal yang mendekati waktu, hentikan pemeriksaan intensif
if (!needIntensiveCheck) {
_stopIntensiveCheck();
}
});
}
// Metode untuk menghentikan pemeriksaan intensif
void _stopIntensiveCheck() {
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
_intensiveCheckActive.value = false;
print('Stopping intensive jadwal check');
}
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update') {
// Update lokal jika jadwal yang diperbarui ada di salah satu list
final jadwalId = updateData['jadwal_id'];
final newStatus = updateData['new_status'];
// Periksa dan update jadwal di berbagai daftar
_updateJadwalStatusLocally(jadwalId, newStatus);
} else if (updateData['type'] == 'reload_required') {
// Muat ulang data jika diminta
loadJadwalData();
loadPermintaanPenjadwalanData();
} else if (updateData['type'] == 'check_required') {
// Segera periksa status jadwal
if (!isLoadingStatusUpdate.value) {
print(
'Received check_required signal, checking jadwal status immediately');
checkAndUpdateJadwalStatus();
} else {
print('Already checking jadwal status, ignoring check_required signal');
}
}
}
// Perbarui status jadwal secara lokal tanpa perlu memanggil API lagi
void _updateJadwalStatusLocally(String jadwalId, String newStatus) {
bool updated = false;
print(
'Updating jadwal status locally - ID: $jadwalId, New Status: $newStatus');
// Periksa jadwal aktif
final jadwalAktifIndex =
jadwalAktif.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalAktifIndex >= 0) {
print('Found in jadwalAktif at index $jadwalAktifIndex');
jadwalAktif[jadwalAktifIndex] =
jadwalAktif[jadwalAktifIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal mendatang
final jadwalMendatangIndex =
jadwalMendatang.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalMendatangIndex >= 0) {
print('Found in jadwalMendatang at index $jadwalMendatangIndex');
jadwalMendatang[jadwalMendatangIndex] =
jadwalMendatang[jadwalMendatangIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal terlaksana
final jadwalTerlaksanaIndex =
jadwalTerlaksana.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalTerlaksanaIndex >= 0) {
print('Found in jadwalTerlaksana at index $jadwalTerlaksanaIndex');
jadwalTerlaksana[jadwalTerlaksanaIndex] =
jadwalTerlaksana[jadwalTerlaksanaIndex].copyWith(status: newStatus);
updated = true;
}
// Jika perlu, reorganisasi daftar berdasarkan status baru
if (updated) {
print('Status updated locally, reorganizing lists');
_reorganizeJadwalLists();
// Perbarui counter penyaluran setelah reorganisasi daftar
_updatePenyaluranCounters();
} else {
print(
'Jadwal with ID $jadwalId not found in any list, refreshing data from server');
// Jika jadwal tidak ditemukan di daftar lokal, muat ulang data
loadJadwalData();
}
}
// Reorganisasi daftar jadwal berdasarkan status mereka
void _reorganizeJadwalLists() {
// Filter jadwal yang seharusnya pindah dari satu list ke list lain
// Jadwal yang seharusnya pindah dari aktif ke terlaksana
final completedJadwal = jadwalAktif
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (completedJadwal.isNotEmpty) {
jadwalAktif.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(completedJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke aktif
final activeJadwal =
jadwalMendatang.where((j) => j.status == 'AKTIF').toList();
if (activeJadwal.isNotEmpty) {
jadwalMendatang.removeWhere((j) => j.status == 'AKTIF');
jadwalAktif.addAll(activeJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke terlaksana
final expiredJadwal = jadwalMendatang
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (expiredJadwal.isNotEmpty) {
jadwalMendatang.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(expiredJadwal);
}
// Memicu pembaruan UI
jadwalAktif.refresh();
jadwalMendatang.refresh();
jadwalTerlaksana.refresh();
}
// Metode baru untuk memperbarui counter penyaluran
void _updatePenyaluranCounters() {
try {
// Dapatkan jumlah jadwal untuk setiap status
int dijadwalkan =
jadwalMendatang.where((j) => j.status == 'DIJADWALKAN').length;
int aktif = jadwalAktif.where((j) => j.status == 'AKTIF').length;
int batal =
jadwalTerlaksana.where((j) => j.status == 'BATALTERLAKSANA').length;
int terlaksana =
jadwalTerlaksana.where((j) => j.status == 'TERLAKSANA').length;
// Hitung total jadwal aktif untuk tab hari ini
int jadwalHariIni = jadwalAktif.length;
// Perbarui counter jadwal
if (Get.isRegistered<CounterService>()) {
final counterService = Get.find<CounterService>();
counterService.updateJadwalCounter(jadwalHariIni);
}
print(
'Jadwal counters updated - Aktif: $aktif, Dijadwalkan: $dijadwalkan, Terlaksana: $terlaksana, Batal: $batal');
} catch (e) {
print('Error updating jadwal counters: $e');
}
} }
// Memeriksa dan memperbarui status jadwal // Memeriksa dan memperbarui status jadwal
Future<void> checkAndUpdateJadwalStatus() async { Future<void> checkAndUpdateJadwalStatus() async {
if (isLoadingStatusUpdate.value) return;
isLoadingStatusUpdate.value = true;
print('Starting jadwal status check at ${DateTime.now()}');
try { try {
final now = DateTime.now(); final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day); final today = DateTime(now.year, now.month, now.day);
List<PenyaluranBantuanModel> jadwalToUpdate = []; // Kelompokkan jadwal yang perlu diperbarui untuk mengurangi jumlah operasi database
List<PenyaluranBantuanModel> jadwalTerlewat = []; final Map<String, String> jadwalUpdates = {};
final List<PenyaluranBantuanModel> jadwalToUpdate = [];
final List<PenyaluranBantuanModel> jadwalTerlewat = [];
for (var jadwal in jadwalAktif) { print('Checking ${jadwalMendatang.length} upcoming schedules');
if (jadwal.tanggalPenyaluran != null) {
final jadwalDateTime =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
final jadwalDate = DateTime(
jadwalDateTime.year,
jadwalDateTime.month,
jadwalDateTime.day,
);
if (isSameDay(jadwalDate, today)) { // Proses semua jadwal yang perlu diperbarui
if (now.isAfter(jadwalDateTime) || for (var jadwal in jadwalMendatang) {
now.isAtSameMomentAs(jadwalDateTime)) { if (jadwal.tanggalPenyaluran != null && jadwal.id != null) {
if (jadwal.status == 'DIJADWALKAN') { final jadwalDate = jadwal.tanggalPenyaluran!;
if (now
.isBefore(jadwalDateTime.add(const Duration(hours: 2)))) { // Log untuk debugging waktu pemeriksaan
await _supabaseService.updateJadwalStatus( print(
jadwal.id!, 'AKTIF'); 'Checking jadwal: ${jadwal.id} - ${jadwal.nama} scheduled for ${jadwal.tanggalPenyaluran}');
jadwalToUpdate.add(jadwal); print('Current time: $now, Jadwal time: $jadwalDate');
} else {
await _supabaseService.updateJadwalStatus( // Periksa apakah jadwal sudah melewati waktunya
jadwal.id!, 'BATALTERLAKSANA'); // Kita gunakan isAtSameMomentAs atau isAfter untuk menangkap dengan tepat
jadwalTerlewat.add(jadwal); if (now.isAfter(jadwalDate) || now.isAtSameMomentAs(jadwalDate)) {
} print('Jadwal time has passed/reached for ${jadwal.id}');
} else if (jadwal.status == 'AKTIF') {
if (now.isAfter(jadwalDateTime.add(const Duration(hours: 2)))) { // Batasan 2 jam untuk status aktif
await _supabaseService.updateJadwalStatus( final batasAktif = jadwalDate.add(const Duration(hours: 2));
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal); if (jadwal.status == 'DIJADWALKAN' && now.isBefore(batasAktif)) {
} print(
'Updating to AKTIF: ${jadwal.id} - Time difference: ${now.difference(jadwalDate).inSeconds} seconds');
jadwalUpdates[jadwal.id!] = 'AKTIF';
jadwalToUpdate.add(jadwal);
} else if ((jadwal.status == 'DIJADWALKAN' ||
jadwal.status == 'AKTIF') &&
now.isAfter(batasAktif)) {
print('Updating to BATALTERLAKSANA (time expired): ${jadwal.id}');
jadwalUpdates[jadwal.id!] = 'BATALTERLAKSANA';
jadwalTerlewat.add(jadwal);
}
} else {
// Periksa apakah jadwal hampir memasuki waktunya (dalam 5 menit ke depan)
final diff = jadwalDate.difference(now).inMinutes;
if (diff >= 0 && diff <= 5 && jadwal.status == 'DIJADWALKAN') {
print('Jadwal will be active in $diff minutes: ${jadwal.id}');
// Tambahkan jadwal ke daftar pengawasan intensif
_jadwalUpdateService.addJadwalToWatch(jadwal.id!, jadwalDate);
// Jika tinggal 1 menit atau kurang, cek setiap 15 detik
if (diff <= 1) {
Future.delayed(const Duration(seconds: 15), () {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
});
} }
} }
} }
} }
} }
if (jadwalToUpdate.isNotEmpty || jadwalTerlewat.isNotEmpty) { // Update database hanya jika ada perubahan
await loadJadwalData(); if (jadwalUpdates.isNotEmpty) {
print('Batch updating ${jadwalUpdates.length} schedules');
if (jadwalToUpdate.isNotEmpty) { try {
Get.snackbar( // Gunakan batch update untuk meningkatkan efisiensi
'Jadwal Diperbarui', await _supabaseService.batchUpdateJadwalStatus(jadwalUpdates);
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
if (jadwalTerlewat.isNotEmpty) { // Perbarui data lokal
Get.snackbar( await loadJadwalData();
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA', // Beritahu seluruh aplikasi tentang pembaruan
snackPosition: SnackPosition.TOP, await _jadwalUpdateService.notifyJadwalUpdate();
backgroundColor: Colors.orange,
colorText: Colors.white, // Kirim notifikasi untuk perubahan status jadwal
duration: const Duration(seconds: 3), bool notificationsSuccessful = true;
); final notificationService = Get.find<NotificationService>();
try {
// Kirim notifikasi untuk jadwal yang diperbarui menjadi Aktif
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'AKTIF',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending AKTIF notifications: $notificationError');
notificationsSuccessful = false;
}
try {
// Kirim notifikasi untuk jadwal yang terlewat
for (var jadwal in jadwalTerlewat) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'BATALTERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending BATALTERLAKSANA notifications: $notificationError');
notificationsSuccessful = false;
}
// Tampilkan notifikasi hanya jika ada perubahan
if (jadwalToUpdate.isNotEmpty) {
Get.snackbar(
'Jadwal Diperbarui',
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
// Log status keseluruhan
if (notificationsSuccessful) {
print(
'Jadwal status update and notifications completed successfully');
} else {
print('Jadwal status update completed with notification errors');
}
} catch (updateError) {
print('Error during batch update process: $updateError');
// Jika batch update gagal, coba update satu-per-satu secara manual
print('Trying individual updates for critical jadwal...');
// Prioritaskan jadwal yang akan diaktifkan
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null) {
try {
await _supabaseService.updateJadwalStatus(jadwal.id!, 'AKTIF');
print('Manual update successful for jadwal ${jadwal.id}');
} catch (e) {
print('Manual update failed for jadwal ${jadwal.id}: $e');
}
}
}
} }
} else {
print('No schedule updates needed');
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
print('Error checking and updating jadwal status: $e'); print('Error checking and updating jadwal status: $e');
print('Stack trace: $stackTrace'); print('Stack trace: $stackTrace');
} finally {
isLoadingStatusUpdate.value = false;
print('Jadwal status check completed at ${DateTime.now()}');
} }
} }
@ -197,6 +563,9 @@ class JadwalPenyaluranController extends GetxController {
.map((data) => PenyaluranBantuanModel.fromJson(data)) .map((data) => PenyaluranBantuanModel.fromJson(data))
.toList(); .toList();
} }
// Perbarui counter penyaluran setelah data dimuat
_updatePenyaluranCounters();
} catch (e) { } catch (e) {
print('Error loading jadwal data: $e'); print('Error loading jadwal data: $e');
} finally { } finally {
@ -220,6 +589,7 @@ class JadwalPenyaluranController extends GetxController {
Future<void> loadLokasiPenyaluranData() async { Future<void> loadLokasiPenyaluranData() async {
try { try {
isLokasiLoading(true);
final lokasiData = await _supabaseService.getAllLokasiPenyaluran(); final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
if (lokasiData != null) { if (lokasiData != null) {
for (var lokasi in lokasiData) { for (var lokasi in lokasiData) {
@ -229,6 +599,8 @@ class JadwalPenyaluranController extends GetxController {
} }
} catch (e) { } catch (e) {
print('Error loading lokasi penyaluran data: $e'); print('Error loading lokasi penyaluran data: $e');
} finally {
isLokasiLoading(false);
} }
} }
@ -335,8 +707,30 @@ class JadwalPenyaluranController extends GetxController {
Future<void> completeJadwal(String jadwalId) async { Future<void> completeJadwal(String jadwalId) async {
isLoading.value = true; isLoading.value = true;
try { try {
// Dapatkan detail jadwal
final jadwalIndex = jadwalAktif.indexWhere((j) => j.id == jadwalId);
PenyaluranBantuanModel? jadwal;
if (jadwalIndex >= 0) {
jadwal = jadwalAktif[jadwalIndex];
}
// Update status di database
await _supabaseService.completeJadwal(jadwalId); await _supabaseService.completeJadwal(jadwalId);
// Kirim notifikasi
if (jadwal != null && jadwal.nama != null) {
final notificationService = Get.find<NotificationService>();
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwalId,
newStatus: 'TERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
// Reload data
await loadJadwalData(); await loadJadwalData();
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Jadwal berhasil diselesaikan', 'Jadwal berhasil diselesaikan',
@ -359,15 +753,13 @@ class JadwalPenyaluranController extends GetxController {
} }
Future<void> refreshData() async { Future<void> refreshData() async {
isLoading.value = true; await Future.wait([
try { loadJadwalData(),
await loadJadwalData(); loadPermintaanPenjadwalanData(),
await loadPermintaanPenjadwalanData(); loadLokasiPenyaluranData(),
} catch (e) { loadKategoriBantuanData(),
print('Error refreshing data: $e'); loadSkemaBantuanData(),
} finally { ]);
isLoading.value = false;
}
} }
void changeCategory(int index) { void changeCategory(int index) {
@ -431,6 +823,7 @@ class JadwalPenyaluranController extends GetxController {
'status_penerimaan': 'BELUMMENERIMA', 'status_penerimaan': 'BELUMMENERIMA',
'qr_code_hash': qrCodeHash, 'qr_code_hash': qrCodeHash,
'jumlah_bantuan': jumlahDiterimaPerOrang, 'jumlah_bantuan': jumlahDiterimaPerOrang,
'created_at': DateTime.now().toIso8601String(),
}; };
// Simpan data penerima ke database // Simpan data penerima ke database

View File

@ -96,10 +96,10 @@ class PelaksanaanPenyaluranController extends GetxController {
? response['kategori_bantuan']['nama'] ? response['kategori_bantuan']['nama']
: 'Tidak tersedia', : 'Tidak tersedia',
'tanggal': penyaluranModel.tanggalPenyaluran != null 'tanggal': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!) ? FormatHelper.formatDateTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia', : 'Tidak tersedia',
'waktu': penyaluranModel.tanggalPenyaluran != null 'waktu': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!) ? FormatHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia', : 'Tidak tersedia',
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0', 'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
'status': penyaluranModel.status, 'status': penyaluranModel.status,

View File

@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
); );
if (picked != null) { if (picked != null) {
tanggalPenyaluran.value = DateTimeHelper.formatDate(picked); tanggalPenyaluran.value = FormatHelper.formatDateTime(picked);
} }
} }

View File

@ -8,6 +8,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_serv
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
class PetugasDesaController extends GetxController { class PetugasDesaController extends GetxController {
final AuthController _authController = Get.find<AuthController>(); final AuthController _authController = Get.find<AuthController>();
@ -182,10 +183,22 @@ class PetugasDesaController extends GetxController {
} }
_counterService = Get.find<CounterService>(); _counterService = Get.find<CounterService>();
// Pastikan JadwalUpdateService juga tersedia
JadwalUpdateService jadwalUpdateService;
if (Get.isRegistered<JadwalUpdateService>()) {
jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Perbarui counter pada saat aplikasi dimulai
jadwalUpdateService.refreshCounters();
// Muat data awal
loadUserProfile(); loadUserProfile();
loadNotifikasiData();
loadJadwalData();
loadPenitipanData(); loadPenitipanData();
loadJadwalData();
loadNotifikasiData();
loadPengaduanData(); loadPengaduanData();
} }

View File

@ -5,11 +5,15 @@ import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'dart:async';
class PetugasDesaDashboardController extends GetxController { class PetugasDesaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>(); final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to; final SupabaseService _supabaseService = SupabaseService.to;
late final CounterService _counterService; late final CounterService _counterService;
late final JadwalUpdateService _jadwalUpdateService;
late StreamSubscription _jadwalUpdateSubscription;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -67,18 +71,47 @@ class PetugasDesaDashboardController extends GetxController {
} }
_counterService = Get.find<CounterService>(); _counterService = Get.find<CounterService>();
// Inisialisasi JadwalUpdateService untuk pembaruan realtime
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('PetugasDesaDashboardController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadUserProfile(); loadUserProfile();
loadDashboardData(); loadDashboardData();
loadNotifikasiData(); loadNotifikasiData();
loadJadwalAktif(); loadJadwalHariIni();
} }
@override @override
void onClose() { void onClose() {
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService
.unregisterFromUpdates('PetugasDesaDashboardController');
searchController.dispose(); searchController.dispose();
super.onClose(); super.onClose();
} }
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update' ||
updateData['type'] == 'reload_required' ||
updateData['type'] == 'check_required') {
// Muat ulang data dashboard saat ada perubahan status jadwal
loadDashboardData();
}
}
// Metode untuk memuat data profil pengguna dari cache // Metode untuk memuat data profil pengguna dari cache
Future<void> loadUserProfile() async { Future<void> loadUserProfile() async {
try { try {
@ -155,14 +188,14 @@ class PetugasDesaDashboardController extends GetxController {
} }
} }
Future<void> loadJadwalAktif() async { Future<void> loadJadwalHariIni() async {
try { try {
final jadwalData = await _supabaseService.getJadwalAktif(); final jadwalData = await _supabaseService.getJadwalAktif();
if (jadwalData != null) { if (jadwalData != null) {
jadwalHariIni.value = jadwalData; jadwalHariIni.value = jadwalData;
} }
} catch (e) { } catch (e) {
print('Error loading jadwal hari ini: $e'); print('Error loading jadwal data: $e');
} }
} }
@ -173,7 +206,7 @@ class PetugasDesaDashboardController extends GetxController {
loadUserProfile(), loadUserProfile(),
loadDashboardData(), loadDashboardData(),
loadNotifikasiData(), loadNotifikasiData(),
loadJadwalAktif(), loadJadwalHariIni(),
]); ]);
} catch (e) { } catch (e) {
print('Error refreshing data: $e'); print('Error refreshing data: $e');

View File

@ -221,15 +221,25 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
), ),
child: CircleAvatar( child: CircleAvatar(
radius: 35, radius: 35,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1), backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
backgroundImage: penerima['foto_profil'] != null backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil']) ? NetworkImage(penerima['foto_profil'])
: null, : null,
child: penerima['foto_profil'] == null child: (penerima['foto_profil'] == null ||
? Icon( penerima['foto_profil'].toString().isEmpty)
Icons.person, ? Text(
size: 35, penerima['nama_lengkap'] != null
color: AppTheme.primaryColor.withOpacity(0.7), ? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
) )
: null, : null,
), ),
@ -435,13 +445,24 @@ class PenerimaSearchDelegate extends SearchDelegate {
}, },
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1), backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil']) ? NetworkImage(penerima['foto_profil'])
: null, : null,
child: penerima['foto_profil'] == null child: (penerima['foto_profil'] == null ||
? const Icon( penerima['foto_profil'].toString().isEmpty)
Icons.person, ? Text(
color: AppTheme.primaryColor, penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
) )
: null, : null,
), ),

View File

@ -33,6 +33,58 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header DisalurKita dengan logo dan slogan
FadeInAnimation(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Header dengan greeting // Header dengan greeting
FadeInAnimation( FadeInAnimation(
child: GreetingHeader( child: GreetingHeader(
@ -83,7 +135,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Jadwal Penyaluran', 'Jadwal Penyaluran Hari Ini',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -130,19 +182,25 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final DateTime tanggal = final DateTime tanggal =
DateTime.parse(jadwal['tanggal_penyaluran']); DateTime.parse(jadwal['tanggal_penyaluran']);
final String formattedDate = final String formattedDate =
DateTimeHelper.formatDateTime(tanggal); FormatHelper.formatDateTime(tanggal);
final kategoriBantuan = final kategoriBantuan =
jadwal['kategori_bantuan'] as Map<String, dynamic>; jadwal['kategori_bantuan'] as Map<String, dynamic>;
final lokasiPenyaluran = final lokasiPenyaluran =
jadwal['lokasi_penyaluran'] as Map<String, dynamic>; jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
return ScheduleCard( return Column(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran', children: [
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia', if (index > 0) const SizedBox(height: 10),
dateTime: formattedDate, ScheduleCard(
isToday: true, title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
onTap: () => Get.toNamed(Routes.detailPenyaluran, location:
parameters: {'id': jadwal['id']}), lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
),
],
); );
}, },
); );
@ -391,8 +449,10 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final nik = penerima['nik'] ?? 'NIK tidak tersedia'; final nik = penerima['nik'] ?? 'NIK tidak tersedia';
final status = penerima['status'] ?? 'AKTIF'; final status = penerima['status'] ?? 'AKTIF';
final id = penerima['id'] ?? 'ID tidak tersedia'; final id = penerima['id'] ?? 'ID tidak tersedia';
final fotoProfil = penerima['foto_profil'] ?? null;
return _buildRecipientItem(name, nik, status, id, textTheme); return _buildRecipientItem(
name, nik, status, id, textTheme, fotoProfil);
}, },
); );
}, },
@ -401,8 +461,8 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
); );
} }
Widget _buildRecipientItem( Widget _buildRecipientItem(String name, String nik, String status, String id,
String name, String nik, String status, String id, TextTheme textTheme) { TextTheme textTheme, String? fotoProfil) {
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
@ -428,7 +488,20 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2), backgroundColor: Colors.white.withOpacity(0.2),
child: const Icon(Icons.person, color: Colors.white), backgroundImage:
fotoProfil != null && fotoProfil.toString().isNotEmpty
? NetworkImage(fotoProfil)
: null,
child: (fotoProfil == null || fotoProfil.toString().isEmpty)
? Text(
name.toString().substring(0, 1).toUpperCase(),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
)
: null,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/donatur_model.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart'; import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailDonaturView extends GetView<DonaturController> { class DetailDonaturView extends GetView<DonaturController> {
const DetailDonaturView({super.key}); const DetailDonaturView({super.key});
@ -359,7 +360,7 @@ class DetailDonaturView extends GetView<DonaturController> {
Icons.calendar_today, Icons.calendar_today,
'Terdaftar Sejak', 'Terdaftar Sejak',
donatur.createdAt != null donatur.createdAt != null
? DateTimeHelper.formatDate(donatur.createdAt!) ? FormatHelper.formatDateTime(donatur.createdAt!)
: 'Tidak diketahui', : 'Tidak diketahui',
), ),
], ],
@ -514,7 +515,8 @@ class DetailDonaturView extends GetView<DonaturController> {
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) { Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
final isUang = penitipan.isUang == true; final isUang = penitipan.isUang == true;
final tanggal = penitipan.createdAt != null final tanggal = penitipan.createdAt != null
? DateTimeHelper.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy') ? FormatHelper.formatDateTime(penitipan.createdAt!,
format: 'dd MMM yyyy')
: 'Tanggal tidak diketahui'; : 'Tanggal tidak diketahui';
String nilaiDonasi = ''; String nilaiDonasi = '';
@ -626,7 +628,7 @@ class DetailDonaturView extends GetView<DonaturController> {
getPetugasDesaNama: (String? id) => getPetugasDesaNama: (String? id) =>
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui', controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
showFullScreenImage: (String imageUrl) { showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl); ShowImageDialog.showFullScreen(Get.context!, imageUrl);
}, },
); );
} }

View File

@ -107,14 +107,24 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: CircleAvatar( child: CircleAvatar(
radius: 60, radius: 60,
backgroundColor: Colors.white, backgroundColor: Colors.white,
backgroundImage: penerima['foto_profil'] != null backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil']) ? NetworkImage(penerima['foto_profil'])
: null, : null,
child: penerima['foto_profil'] == null child: (penerima['foto_profil'] == null ||
? Icon( penerima['foto_profil'].toString().isEmpty)
Icons.person, ? Text(
size: 60, penerima['nama_lengkap'] != null
color: AppTheme.primaryColor.withOpacity(0.7), ? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor.withOpacity(0.7),
fontSize: 36,
),
) )
: null, : null,
), ),
@ -507,7 +517,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: _buildInfoItem( child: _buildInfoItem(
Icons.calendar_today, Icons.calendar_today,
'Tanggal Penerimaan', 'Tanggal Penerimaan',
DateTimeHelper.formatDateTime(tanggalPenerimaan), FormatHelper.formatDateTime(tanggalPenerimaan),
), ),
), ),
Expanded( Expanded(

View File

@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart'; import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart'; import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:timeline_tile/timeline_tile.dart'; import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -15,7 +14,7 @@ import 'dart:io';
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart'; import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart'; import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailPengaduanView extends GetView<PengaduanController> { class DetailPengaduanView extends GetView<PengaduanController> {
const DetailPengaduanView({super.key}); const DetailPengaduanView({super.key});
@ -1092,8 +1091,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
child: Row( child: Row(
children: tindakan.buktiTindakan!.map((bukti) { children: tindakan.buktiTindakan!.map((bukti) {
return GestureDetector( return GestureDetector(
onTap: () => onTap: () => ShowImageDialog.showFullScreen(
showFullScreenImage(context, bukti), context, bukti),
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
@ -1190,8 +1189,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
Expanded( Expanded(
child: Text( child: Text(
tindakan.tanggalTindakan != null tindakan.tanggalTindakan != null
? DateFormat('dd MMM yyyy HH:mm', 'id_ID') ? FormatHelper.formatDateTime(
.format(tindakan.tanggalTindakan!) tindakan.tanggalTindakan!)
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -1669,9 +1668,11 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Stack( return Stack(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () => showFullScreenImage( onTap: () => ShowImageDialog
stateContext, .showFullScreen(
buktiTindakanPaths[index]), stateContext,
buktiTindakanPaths[
index]),
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
@ -2003,63 +2004,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
); );
} }
void showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black87,
),
),
InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(20),
minScale: 0.5,
maxScale: 4.0,
child: CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, color: Colors.white, size: 32),
const SizedBox(height: 8),
Text(
'Gagal memuat gambar',
style: TextStyle(color: Colors.white),
),
],
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
// Widget untuk menampilkan feedback dan rating warga
Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) { Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) {
return Card( return Card(
elevation: 3, elevation: 3,
@ -2348,8 +2292,7 @@ class DetailPengaduanView extends GetView<PengaduanController> {
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
pengaduan.tanggalPengaduan != null pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
.format(pengaduan.tanggalPengaduan!)
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
@ -2376,7 +2319,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: GestureDetector( child: GestureDetector(
onTap: () => _showFullScreenImage(context, url), onTap: () =>
ShowImageDialog.showFullScreen(context, url),
child: Container( child: Container(
width: 120, width: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -2589,57 +2533,4 @@ class DetailPengaduanView extends GetView<PengaduanController> {
); );
} }
} }
void _showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.7),
child: Center(
child: imagePath.startsWith('http')
? CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
)
: Image.file(File(imagePath)),
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
size: 30,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
} }

View File

@ -267,7 +267,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event, Icons.event,
'Tanggal Penyaluran', 'Tanggal Penyaluran',
penyaluran.tanggalPenyaluran != null penyaluran.tanggalPenyaluran != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
penyaluran.tanggalPenyaluran!) penyaluran.tanggalPenyaluran!)
: 'Belum dijadwalkan', : 'Belum dijadwalkan',
AppTheme.secondaryColor), AppTheme.secondaryColor),
@ -280,7 +280,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event_available, Icons.event_available,
'Tanggal Selesai', 'Tanggal Selesai',
penyaluran.tanggalSelesai != null penyaluran.tanggalSelesai != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!) penyaluran.tanggalSelesai!)
: '-', : '-',
AppTheme.secondaryColor), AppTheme.secondaryColor),
@ -1065,19 +1065,30 @@ class DetailPenyaluranPage extends StatelessWidget {
backgroundColor: sudahMenerima backgroundColor: sudahMenerima
? statusColor.withOpacity(0.15) ? statusColor.withOpacity(0.15)
: Colors.grey.shade50, : Colors.grey.shade50,
child: Text( backgroundImage: warga != null &&
warga != null && warga['nama_lengkap'] != null warga['foto_profil'] != null &&
? warga['nama_lengkap'] warga['foto_profil'].toString().isNotEmpty
.toString() ? NetworkImage(warga['foto_profil'])
.substring(0, 1) : null,
.toUpperCase() child: (warga == null ||
: '?', warga['foto_profil'] == null ||
style: TextStyle( warga['foto_profil'].toString().isEmpty)
fontWeight: FontWeight.bold, ? Text(
color: sudahMenerima ? statusColor : Colors.grey.shade700, warga != null && warga['nama_lengkap'] != null
fontSize: 22, ? warga['nama_lengkap']
), .toString()
), .substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: sudahMenerima
? statusColor
: Colors.grey.shade700,
fontSize: 22,
),
)
: null,
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -1621,19 +1632,28 @@ class DetailPenyaluranPage extends StatelessWidget {
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundColor: statusColor.withOpacity(0.2), backgroundColor: statusColor.withOpacity(0.2),
child: Text( backgroundImage: warga != null &&
warga != null && warga['nama_lengkap'] != null warga['foto_profil'] != null &&
? warga['nama_lengkap'] warga['foto_profil'].toString().isNotEmpty
.toString() ? NetworkImage(warga['foto_profil'])
.substring(0, 1) : null,
.toUpperCase() child: (warga == null ||
: '?', warga['foto_profil'] == null ||
style: TextStyle( warga['foto_profil'].toString().isEmpty)
fontWeight: FontWeight.bold, ? Text(
color: statusColor, warga != null && warga['nama_lengkap'] != null
fontSize: 24, ? warga['nama_lengkap']
), .toString()
), .substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
fontSize: 24,
),
)
: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@ -1753,7 +1773,7 @@ class DetailPenyaluranPage extends StatelessWidget {
if (penerima.tanggalPenerimaan != null) if (penerima.tanggalPenerimaan != null)
_buildInfoRow( _buildInfoRow(
'Tanggal Penerimaan', 'Tanggal Penerimaan',
DateTimeHelper.formatDate( FormatHelper.formatDateTime(
penerima.tanggalPenerimaan!)), penerima.tanggalPenerimaan!)),
if (penerima.jumlahBantuan != null) if (penerima.jumlahBantuan != null)
_buildInfoRow('Jumlah Bantuan', _buildInfoRow('Jumlah Bantuan',
@ -1946,7 +1966,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow('Status', 'Batal Terlaksana'), _buildInfoRow('Status', 'Batal Terlaksana'),
if (penyaluran.tanggalSelesai != null) if (penyaluran.tanggalSelesai != null)
_buildInfoRow('Tanggal Pembatalan', _buildInfoRow('Tanggal Pembatalan',
DateTimeHelper.formatDateTime(penyaluran.tanggalSelesai!)), FormatHelper.formatDateTime(penyaluran.tanggalSelesai!)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'Alasan Pembatalan:', 'Alasan Pembatalan:',
@ -2126,7 +2146,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow( _buildInfoRow(
'Tanggal Laporan', 'Tanggal Laporan',
controller.laporan.value?.tanggalLaporan != null controller.laporan.value?.tanggalLaporan != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
controller.laporan.value!.tanggalLaporan!) controller.laporan.value!.tanggalLaporan!)
: '-', : '-',
), ),

View File

@ -198,7 +198,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
'Tempat, Tanggal Lahir', 'Tempat, Tanggal Lahir',
warga?['tempat_lahir'] != null && warga?['tempat_lahir'] != null &&
warga?['tanggal_lahir'] != null warga?['tanggal_lahir'] != null
? '${warga!['tempat_lahir']}, ${DateTimeHelper.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}' ? '${warga!['tempat_lahir']}, ${FormatHelper.formatDateTime(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
: 'Bogor, 2 Juni 1990'), : 'Bogor, 2 Juni 1990'),
const Divider(), const Divider(),
@ -236,18 +236,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
String tanggalWaktuPenyaluran = ''; String tanggalWaktuPenyaluran = '';
if (widget.tanggalPenyaluran != null) { if (widget.tanggalPenyaluran != null) {
final tanggal = DateTimeHelper.formatDate(widget.tanggalPenyaluran!); final tanggal = FormatHelper.formatDateTime(widget.tanggalPenyaluran!);
final waktuMulai = DateTimeHelper.formatTime(widget.tanggalPenyaluran!); final waktuMulai = FormatHelper.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = DateTimeHelper.formatTime( final waktuSelesai = FormatHelper.formatTime(
widget.tanggalPenyaluran!.add(const Duration(hours: 1))); widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai'; tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else if (penerima.penyaluranBantuan != null && } else if (penerima.penyaluranBantuan != null &&
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) { penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
final tanggalPenyaluran = final tanggalPenyaluran =
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']); DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran); final tanggal = FormatHelper.formatDateTime(tanggalPenyaluran);
final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran); final waktuMulai = FormatHelper.formatTime(tanggalPenyaluran);
final waktuSelesai = DateTimeHelper.formatTime( final waktuSelesai = FormatHelper.formatTime(
tanggalPenyaluran.add(const Duration(hours: 1))); tanggalPenyaluran.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai'; tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else { } else {

View File

@ -44,7 +44,7 @@ class PengaduanView extends GetView<PengaduanController> {
Widget _buildLastUpdateInfo(BuildContext context) { Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime final lastUpdate = DateTime
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia .now(); // Gunakan waktu saat ini atau dari controller jika tersedia
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate); final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
@ -280,7 +280,7 @@ class PengaduanView extends GetView<PengaduanController> {
), ),
), ),
Text( Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item', '${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
), ),
@ -320,7 +320,7 @@ class PengaduanView extends GetView<PengaduanController> {
// Format tanggal menggunakan DateTimeHelper // Format tanggal menggunakan DateTimeHelper
String formattedDate = ''; String formattedDate = '';
if (item.tanggalPengaduan != null) { if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan); formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
} }
return Card( return Card(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_ba
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart'; import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
import 'dart:io'; import 'dart:io';
class PenitipanView extends GetView<PenitipanBantuanController> { class PenitipanView extends GetView<PenitipanBantuanController> {
@ -72,7 +73,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context, context,
icon: Icons.pending_actions, icon: Icons.pending_actions,
title: 'Menunggu', title: 'Menunggu',
value: DateTimeHelper.formatNumber( value: FormatHelper.formatNumber(
controller.jumlahMenunggu.value), controller.jumlahMenunggu.value),
color: Colors.orange, color: Colors.orange,
), ),
@ -82,7 +83,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context, context,
icon: Icons.check_circle, icon: Icons.check_circle,
title: 'Terverifikasi', title: 'Terverifikasi',
value: DateTimeHelper.formatNumber( value: FormatHelper.formatNumber(
controller.jumlahTerverifikasi.value), controller.jumlahTerverifikasi.value),
color: Colors.green, color: Colors.green,
), ),
@ -92,8 +93,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context, context,
icon: Icons.cancel, icon: Icons.cancel,
title: 'Ditolak', title: 'Ditolak',
value: DateTimeHelper.formatNumber( value:
controller.jumlahDitolak.value), FormatHelper.formatNumber(controller.jumlahDitolak.value),
color: Colors.red, color: Colors.red,
), ),
), ),
@ -219,7 +220,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
), ),
), ),
Text( Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item', '${FormatHelper.formatNumber(filteredList.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
), ),
@ -360,7 +361,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
], ],
), ),
Text( Text(
DateTimeHelper.formatDate(item.createdAt), FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700, color: Colors.grey.shade700,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
@ -380,15 +381,27 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Row( Row(
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20, radius: 20,
child: Text( backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
donaturNama.substring(0, 1).toUpperCase(), backgroundImage: item.donatur != null &&
style: TextStyle( item.donatur!.fotoProfil != null &&
color: AppTheme.primaryColor, item.donatur!.fotoProfil!.isNotEmpty
fontWeight: FontWeight.bold, ? NetworkImage(item.donatur!.fotoProfil!)
), : null,
), child: (item.donatur == null ||
item.donatur!.fotoProfil == null ||
item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 16,
),
)
: null,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@ -546,8 +559,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
isUang isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}' ? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan', : '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleSmall .titleSmall
@ -947,7 +960,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
kategoriSatuan: kategoriSatuan, kategoriSatuan: kategoriSatuan,
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id), getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
showFullScreenImage: (String imageUrl) { showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(context, imageUrl); ShowImageDialog.showFullScreen(context, imageUrl);
}, },
); );
} }
@ -992,7 +1005,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) { Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() { return Obx(() {
final lastUpdate = controller.lastUpdateTime.value; final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate); final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
class PenyaluranView extends GetView<JadwalPenyaluranController> { class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key}); const PenyaluranView({super.key});
@ -41,13 +42,20 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
), ),
], ],
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: Column(
onPressed: () => Get.to(() => const TambahPenyaluranView()), mainAxisSize: MainAxisSize.min,
backgroundColor: AppTheme.primaryColor, children: [
icon: const Icon(Icons.add, color: Colors.white), // Tombol untuk menambah jadwal penyaluran
label: const Text('Tambah Jadwal', FloatingActionButton.extended(
style: TextStyle(color: Colors.white)), heroTag: 'tambahJadwal',
elevation: 2, onPressed: () => Get.to(() => const TambahPenyaluranView()),
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Jadwal',
style: TextStyle(color: Colors.white)),
elevation: 2,
),
],
), ),
), ),
); );
@ -76,6 +84,11 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Ringkasan jadwal // Ringkasan jadwal
_buildJadwalSummary(Get.context!), _buildJadwalSummary(Get.context!),
const SizedBox(height: 16),
// Tombol untuk mengelola lokasi penyaluran
_buildLokasiPenyaluranSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
// Jadwal hari ini // Jadwal hari ini
@ -224,4 +237,240 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
], ],
); );
} }
// Widget untuk menampilkan section lokasi penyaluran
Widget _buildLokasiPenyaluranSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.blue.shade100, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Lokasi Penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
OutlinedButton.icon(
onPressed: () {
// Menampilkan dialog daftar lokasi penyaluran
_showLokasiPenyaluranDialog();
},
icon: const Icon(Icons.map, size: 16),
label: const Text('Lihat Lokasi'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue.shade300),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
),
],
),
const SizedBox(height: 8),
Text(
'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran),
icon: const Icon(Icons.add_location, size: 16),
label: const Text('Tambah Lokasi Penyaluran Baru'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade50,
foregroundColor: Colors.blue.shade700,
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.blue.shade200),
),
),
),
],
),
),
);
}
// Fungsi untuk menampilkan dialog daftar lokasi penyaluran
void _showLokasiPenyaluranDialog() {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Lokasi Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: 12),
Container(
constraints: BoxConstraints(
maxHeight: Get.height * 0.5,
),
width: double.infinity,
child: Obx(() {
if (controller.isLokasiLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (controller.lokasiPenyaluranCache.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_off,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum ada lokasi penyaluran',
style: TextStyle(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
icon: const Icon(Icons.add_location),
label: const Text('Tambah Lokasi'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: controller.lokasiPenyaluranCache.length,
itemBuilder: (context, index) {
final lokasi = controller.lokasiPenyaluranCache.values
.elementAt(index);
final lokasiId = controller.lokasiPenyaluranCache.keys
.elementAt(index);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(
lokasi.nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (lokasi.alamat != null &&
lokasi.alamat!.isNotEmpty)
Text(lokasi.alamat!),
Row(
children: [
if (lokasi.isLokasiTitip)
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Lokasi Penitipan',
style: TextStyle(
fontSize: 10,
color: Colors.green.shade800,
),
),
),
],
),
],
),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
shape: BoxShape.circle,
),
child: Icon(
Icons.location_on,
color: Colors.blue.shade700,
),
),
),
);
},
);
}),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
child: const Text('Tambah Lokasi Baru'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
),
),
],
),
],
),
),
),
);
}
} }

View File

@ -39,7 +39,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 4: case 4:
return const Text('Stok Bantuan'); return const Text('Stok Bantuan');
default: default:
return const Text('Petugas Desa'); return const Text('Dashboard');
} }
}), }),
leading: IconButton( leading: IconButton(
@ -223,14 +223,23 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
child: CircleAvatar( child: CircleAvatar(
radius: 40, radius: 40,
backgroundColor: Colors.white70, backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!) ? NetworkImage(controller.profilePhotoUrl!)
: null, : null,
child: controller.profilePhotoUrl == null child: (controller.profilePhotoUrl == null ||
? Icon( controller.profilePhotoUrl!.isEmpty)
Icons.person, ? Text(
color: Colors.white, controller.nama.isNotEmpty
size: 40, ? controller.nama
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 30,
),
) )
: null, : null,
), ),
@ -396,6 +405,16 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Get.toNamed('/profile'); Get.toNamed('/profile');
}, },
), ),
const Divider(),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem( _buildMenuItem(
icon: Icons.logout, icon: Icons.logout,
title: 'Keluar', title: 'Keluar',
@ -411,7 +430,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text( child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan', '© ${DateTime.now().year} DisalurKita',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey, color: Colors.grey,

View File

@ -43,7 +43,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Tambahkan widget untuk menampilkan waktu terakhir update // Tambahkan widget untuk menampilkan waktu terakhir update
Widget _buildLastUpdateInfo(BuildContext context) { Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime.now(); final lastUpdate = DateTime.now();
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate); final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
@ -135,7 +135,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
), ),
), ),
Text( Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item', '${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
), ),
@ -154,9 +154,9 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Format tanggal menggunakan DateTimeHelper // Format tanggal menggunakan DateTimeHelper
String formattedDate = ''; String formattedDate = '';
if (item.tanggalPengaduan != null) { if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan); formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
} else if (item.createdAt != null) { } else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt); formattedDate = FormatHelper.formatDateTime(item.createdAt);
} }
Color statusColor = AppTheme.successColor; Color statusColor = AppTheme.successColor;

View File

@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> { class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const RiwayatPenitipanView({super.key}); const RiwayatPenitipanView({super.key});
@ -47,7 +48,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? ''; final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? ''; final deskripsi = item.deskripsi?.toLowerCase() ?? '';
final tanggal = final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenitipan).toLowerCase(); FormatHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
return donaturNama.contains(searchText) || return donaturNama.contains(searchText) ||
kategoriNama.contains(searchText) || kategoriNama.contains(searchText) ||
@ -99,7 +100,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
), ),
), ),
Text( Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item', '${FormatHelper.formatNumber(filteredList.length)} item',
style: style:
Theme.of(context).textTheme.bodyMedium?.copyWith( Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
@ -113,7 +114,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item', 'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style: style:
Theme.of(context).textTheme.bodyMedium?.copyWith( Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
@ -126,7 +127,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
size: 16, color: Colors.grey[600]), size: 16, color: Colors.grey[600]),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}', 'Update: ${FormatHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Colors.grey[600],
@ -262,7 +263,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
], ],
), ),
Text( Text(
DateTimeHelper.formatDate(item.createdAt), FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700, color: Colors.grey.shade700,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
@ -282,17 +283,26 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
Row( Row(
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20, radius: 20,
child: Text( backgroundColor: statusColor.withOpacity(0.2),
donaturNama.isNotEmpty backgroundImage: item.donatur != null &&
? donaturNama.substring(0, 1).toUpperCase() item.donatur!.fotoProfil != null &&
: '?', item.donatur!.fotoProfil!.isNotEmpty
style: TextStyle( ? NetworkImage(item.donatur!.fotoProfil!)
color: AppTheme.primaryColor, : null,
fontWeight: FontWeight.bold, child: (item.donatur == null ||
), item.donatur!.fotoProfil == null ||
), item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
),
)
: null,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@ -422,8 +432,8 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
isUang isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}' ? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan', : '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleSmall .titleSmall
@ -579,20 +589,20 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
_buildDetailItem( _buildDetailItem(
'Jumlah', 'Jumlah',
isUang isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}' ? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan'), : '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'), if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
_buildDetailItem( _buildDetailItem(
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'), 'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
_buildDetailItem( _buildDetailItem(
'Tanggal Penitipan', 'Tanggal Penitipan',
DateTimeHelper.formatDateTime(item.tanggalPenitipan, FormatHelper.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'), defaultValue: 'Tidak ada tanggal'),
), ),
if (item.tanggalVerifikasi != null) if (item.tanggalVerifikasi != null)
_buildDetailItem( _buildDetailItem(
'Tanggal Verifikasi', 'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi), FormatHelper.formatDateTime(item.tanggalVerifikasi),
), ),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem( _buildDetailItem(
@ -600,7 +610,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
controller.getPetugasDesaNama(item.petugasDesaId), controller.getPetugasDesaNama(item.petugasDesaId),
), ),
_buildDetailItem('Tanggal Dibuat', _buildDetailItem('Tanggal Dibuat',
DateTimeHelper.formatDateTime(item.createdAt)), FormatHelper.formatDateTime(item.createdAt)),
if (item.alasanPenolakan != null && if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty) item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!), _buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
@ -626,8 +636,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_showFullScreenImage( ShowImageDialog.show(
context, item.fotoBantuan![index]); context,
item.fotoBantuan![index],
);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
@ -677,8 +689,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_showFullScreenImage( ShowImageDialog.show(
context, item.fotoBantuan![index]); context,
item.fotoBantuan![index],
);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
@ -721,8 +735,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
_showFullScreenImage( ShowImageDialog.show(
context, item.fotoBuktiSerahTerima!); context,
item.fotoBuktiSerahTerima!,
);
}, },
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -757,58 +773,6 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
); );
} }
void _showFullScreenImage(BuildContext context, String imageUrl) {
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
fit: StackFit.expand,
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey.shade300,
child: const Center(
child: Icon(
Icons.error,
size: 50,
color: Colors.red,
),
),
);
},
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
),
),
),
),
],
),
),
);
}
Widget _buildDetailItem(String label, String value) { Widget _buildDetailItem(String label, String value) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),

View File

@ -52,7 +52,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
.getKategoriBantuanName(item.kategoriBantuanId) .getKategoriBantuanName(item.kategoriBantuanId)
.toLowerCase(); .toLowerCase();
final tanggal = final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase(); FormatHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
return nama.contains(searchText) || return nama.contains(searchText) ||
deskripsi.contains(searchText) || deskripsi.contains(searchText) ||
@ -105,7 +105,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
), ),
), ),
Text( Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item', '${FormatHelper.formatNumber(filteredList.length)} item',
style: style:
Theme.of(context).textTheme.bodyMedium?.copyWith( Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
@ -119,7 +119,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item', 'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style: style:
Theme.of(context).textTheme.bodyMedium?.copyWith( Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey, color: Colors.grey,
@ -132,7 +132,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
size: 16, color: Colors.grey[600]), size: 16, color: Colors.grey[600]),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(DateTime.now())}', 'Update: ${FormatHelper.formatDateTimeWithHour(DateTime.now())}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Colors.grey[600],
@ -305,7 +305,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
child: _buildInfoItem( child: _buildInfoItem(
Icons.event, Icons.event,
'Tanggal', 'Tanggal',
DateTimeHelper.formatDateTime(item.tanggalPenyaluran, FormatHelper.formatDateTime(item.tanggalPenyaluran,
format: 'dd MMM yyyy HH:mm'), format: 'dd MMM yyyy HH:mm'),
Theme.of(context).textTheme, Theme.of(context).textTheme,
), ),
@ -316,17 +316,57 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
_buildInfoItem( _buildInfoItem(
Icons.people_outline, Icons.people_outline,
'Jumlah Penerima', 'Jumlah Penerima',
'${DateTimeHelper.formatNumber(item.jumlahPenerima ?? 0)} orang', '${FormatHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
Theme.of(context).textTheme, Theme.of(context).textTheme,
), ),
if (item.alasanPembatalan != null && if (item.alasanPembatalan != null &&
item.alasanPembatalan!.isNotEmpty) ...[ item.alasanPembatalan!.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 12),
_buildInfoItem( Container(
Icons.info_outline, padding: const EdgeInsets.all(10),
'Alasan Pembatalan', decoration: BoxDecoration(
item.alasanPembatalan!, color: Colors.red.shade50,
Theme.of(context).textTheme, borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.cancel_outlined,
size: 20,
color: Colors.red.shade700,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alasan Pembatalan',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
const SizedBox(height: 4),
Text(
item.alasanPembatalan!,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Colors.red.shade800,
),
),
],
),
),
],
),
), ),
], ],
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@ -6,6 +6,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatStokView extends GetView<RiwayatStokController> { class RiwayatStokView extends GetView<RiwayatStokController> {
const RiwayatStokView({super.key}); const RiwayatStokView({super.key});
@ -353,7 +354,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
); );
}).toList(), }),
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
@ -543,7 +544,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
riwayat.createdAt != null riwayat.createdAt != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
riwayat.createdAt!) riwayat.createdAt!)
: '-', : '-',
style: TextStyle( style: TextStyle(
@ -598,7 +599,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: const EdgeInsets.only(left: 44), padding: const EdgeInsets.only(left: 44),
child: InkWell( child: InkWell(
onTap: () => onTap: () =>
_showImageDialog(context, riwayat.fotoBukti!), ShowImageDialog.show(context, riwayat.fotoBukti!),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1), color: Colors.blue.withOpacity(0.1),
@ -704,97 +705,6 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
); );
} }
void _showImageDialog(BuildContext context, String imageUrl) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
leading: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'Bukti Foto',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
elevation: 0,
backgroundColor: AppTheme.primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(16),
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl: imageUrl,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Gagal memuat gambar: $error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
],
),
fit: BoxFit.contain,
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.zoom_in, size: 20, color: Colors.grey),
const SizedBox(width: 8),
Text(
'Cubit untuk memperbesar/memperkecil',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
);
},
);
}
void _showStokManualDialog(BuildContext context, {required bool isAddition}) { void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
// Reset form // Reset form
controller.resetForm(); controller.resetForm();
@ -1152,7 +1062,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenitipanDetail( Widget _buildPenitipanDetail(
BuildContext context, Map<String, dynamic> data) { BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at'])) ? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-'; : '-';
final String namaPenitip = data['donatur'] != null final String namaPenitip = data['donatur'] != null
@ -1357,7 +1267,8 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: index < fotoBantuan.length - 1 ? 8.0 : 0), right: index < fotoBantuan.length - 1 ? 8.0 : 0),
child: InkWell( child: InkWell(
onTap: () => _showImageDialog(context, imageUrl), onTap: () =>
ShowImageDialog.show(context, imageUrl),
child: Container( child: Container(
width: 200, width: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -1442,7 +1353,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenerimaanDetail( Widget _buildPenerimaanDetail(
BuildContext context, Map<String, dynamic> data) { BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at'])) ? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-'; : '-';
final String namaPenerima = data['warga'] != null final String namaPenerima = data['warga'] != null
@ -1646,7 +1557,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
InkWell( InkWell(
onTap: () => _showImageDialog(context, buktiPenerimaan), onTap: () => ShowImageDialog.show(context, buktiPenerimaan),
child: Container( child: Container(
height: 180, height: 180,
width: double.infinity, width: double.infinity,

View File

@ -156,7 +156,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
), ),
), ),
Text( Text(
'Rp ${DateTimeHelper.formatNumber(controller.totalDanaBantuan.value)}', 'Rp ${FormatHelper.formatNumber(controller.totalDanaBantuan.value)}',
style: style:
Theme.of(context).textTheme.titleLarge?.copyWith( Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -512,8 +512,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
), ),
Text( Text(
item.isUang == true item.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}' ? 'Rp ${FormatHelper.formatNumber(item.totalStok)}'
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}', : '${FormatHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleLarge .titleLarge
@ -549,7 +549,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded( Expanded(
child: Text( child: Text(
item.updatedAt != null item.updatedAt != null
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}' ? 'Diperbarui: ${FormatHelper.formatDateTimeWithHour(item.updatedAt!)}'
: 'Tidak ada data pembaruan', : 'Tidak ada data pembaruan',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600], color: Colors.grey[600],
@ -984,8 +984,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
isUang isUang
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}' ? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}', : '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
], ],
@ -1175,8 +1175,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
stok.isUang == true stok.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}' ? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}', : '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
], ],
@ -1240,7 +1240,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) { Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() { return Obx(() {
final lastUpdate = controller.lastUpdateTime.value; final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate); final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),

View File

@ -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,
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -231,12 +231,39 @@ class ProfileView extends GetView<ProfileController> {
Widget _buildDefaultProfileImage() { Widget _buildDefaultProfileImage() {
return CircleAvatar( return CircleAvatar(
radius: 60, radius: 60,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1), backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
child: const Icon( child: Obx(() {
Icons.person, final user = controller.user.value;
size: 70, final roleData = controller.roleData.value;
color: AppTheme.primaryColor,
), String displayInitial = '?';
if (roleData != null && roleData.isNotEmpty) {
final roleDataValue = roleData;
if (roleDataValue['nama_lengkap'] != null &&
roleDataValue['nama_lengkap'].toString().isNotEmpty) {
displayInitial = roleDataValue['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase();
} else if (roleDataValue['nama'] != null &&
roleDataValue['nama'].toString().isNotEmpty) {
displayInitial =
roleDataValue['nama'].toString().substring(0, 1).toUpperCase();
}
} else if (user != null && user.name != null && user.name!.isNotEmpty) {
displayInitial = user.name!.substring(0, 1).toUpperCase();
}
return Text(
displayInitial,
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 60,
),
);
}),
); );
} }

View File

@ -33,23 +33,20 @@ class _SplashViewState extends State<SplashView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
child: Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Image.asset( Image.asset(
'assets/images/logo.png', 'assets/images/logo-disalurkita.png',
width: 120, width: 150,
height: 120, height: 150,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Container( return Container(
width: 120, width: 120,
height: 120, height: 120,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: const Icon( child: const Icon(
@ -62,24 +59,25 @@ class _SplashViewState extends State<SplashView> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const Text( const Text(
'Aplikasi Penyaluran', 'DisalurKita',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: AppTheme.primaryColor,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'Bantuan Sosial', 'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 16,
color: Colors.white, color: AppTheme.primaryColor,
), ),
), ),
const SizedBox(height: 48), const SizedBox(height: 48),
const CircularProgressIndicator( const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), valueColor:
AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
), ),
], ],
), ),

View File

@ -1,16 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:timeline_tile/timeline_tile.dart'; import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart'; import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart'; import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'dart:io'; import 'dart:io';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class WargaDetailPengaduanView extends GetView<WargaDashboardController> { class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
const WargaDetailPengaduanView({super.key}); const WargaDetailPengaduanView({super.key});
@ -670,8 +670,7 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
pengaduan.tanggalPengaduan != null pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
.format(pengaduan.tanggalPengaduan!)
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
@ -1309,8 +1308,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
child: Row( child: Row(
children: tindakan.buktiTindakan!.map((bukti) { children: tindakan.buktiTindakan!.map((bukti) {
return GestureDetector( return GestureDetector(
onTap: () => onTap: () => ShowImageDialog.showFullScreen(
showFullScreenImage(context, bukti), context, bukti),
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
@ -1407,8 +1406,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
Expanded( Expanded(
child: Text( child: Text(
tindakan.tanggalTindakan != null tindakan.tanggalTindakan != null
? DateFormat('dd MMM yyyy HH:mm', 'id_ID') ? FormatHelper.formatDateTime(
.format(tindakan.tanggalTindakan!) tindakan.tanggalTindakan!)
: '-', : '-',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -1429,183 +1428,8 @@ class WargaDetailPengaduanView extends GetView<WargaDashboardController> {
); );
} }
void showFullScreenImage(BuildContext context, String imageUrl) { void _showFullScreenImage(BuildContext context, String imagePath) {
// Buat controller untuk InteractiveViewer ShowImageDialog.showFullScreen(context, imagePath);
final TransformationController transformationController =
TransformationController();
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Colors.black,
child: InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
transformationController: transformationController,
child: Center(
child: Hero(
tag: imageUrl,
child: imageUrl.startsWith('http')
? Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.broken_image,
size: 60,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
)
: Image.file(
File(imageUrl),
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.broken_image,
size: 60,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
),
),
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
),
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildImageControlButton(
icon: Icons.zoom_out,
onTap: () {
// Zoom out
final Matrix4 matrix =
transformationController.value.clone();
matrix.scale(0.75);
transformationController.value = matrix;
},
),
const SizedBox(width: 16),
_buildImageControlButton(
icon: Icons.refresh,
onTap: () {
// Reset
transformationController.value = Matrix4.identity();
},
),
const SizedBox(width: 16),
_buildImageControlButton(
icon: Icons.zoom_in,
onTap: () {
// Zoom in
final Matrix4 matrix =
transformationController.value.clone();
matrix.scale(1.5);
transformationController.value = matrix;
},
),
],
),
),
],
),
),
);
}
Widget _buildImageControlButton({
required IconData icon,
required Function() onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
);
} }
} }
@ -2056,8 +1880,7 @@ class _TambahTindakanPengaduanViewState
} }
void _showFullScreenImage(BuildContext context, String imagePath) { void _showFullScreenImage(BuildContext context, String imagePath) {
final wargaDetailView = Get.find<WargaDetailPengaduanView>(); ShowImageDialog.showFullScreen(context, imagePath);
wargaDetailView.showFullScreenImage(context, imagePath);
} }
Future<void> _simpanTindakan() async { Future<void> _simpanTindakan() async {
@ -2078,22 +1901,6 @@ class _TambahTindakanPengaduanViewState
}); });
try { try {
// Di sini kita baru melakukan upload file ke server
// Contoh implementasi:
// 1. Upload semua file bukti tindakan
// final List<String> buktiTindakanUrls = await uploadMultipleFiles(buktiTindakanPaths);
// 2. Simpan data tindakan ke database
// await saveTindakanPengaduan(
// pengaduanId: widget.pengaduanId,
// kategoriTindakan: selectedKategori!,
// prioritas: selectedPrioritas!,
// tindakan: tindakanController.text,
// catatan: catatanController.text,
// buktiTindakanUrls: buktiTindakanUrls,
// );
// Tampilkan pesan sukses // Tampilkan pesan sukses
Get.back(); // Kembali ke halaman sebelumnya Get.back(); // Kembali ke halaman sebelumnya
Get.snackbar( Get.snackbar(

View File

@ -11,12 +11,12 @@ class FormPengaduanView extends StatefulWidget {
final List<File>? selectedImages; final List<File>? selectedImages;
const FormPengaduanView({ const FormPengaduanView({
Key? key, super.key,
required this.uidPenerimaan, required this.uidPenerimaan,
this.judul, this.judul,
this.deskripsi, this.deskripsi,
this.selectedImages, this.selectedImages,
}) : super(key: key); });
@override @override
State<FormPengaduanView> createState() => _FormPengaduanViewState(); State<FormPengaduanView> createState() => _FormPengaduanViewState();
@ -219,7 +219,7 @@ class _FormPengaduanViewState extends State<FormPengaduanView> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( SizedBox(
height: 120, height: 120,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart'; import 'package:penyaluran_app/app/widgets/section_header.dart';
class WargaDashboardView extends GetView<WargaDashboardController> { class WargaDashboardView extends GetView<WargaDashboardController> {
@ -23,6 +23,54 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header DisalurKita dengan logo dan slogan
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
_buildWelcomeSection(), _buildWelcomeSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildStatisticSection(), _buildStatisticSection(),
@ -90,10 +138,17 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
? NetworkImage(controller.profilePhotoUrl!) ? NetworkImage(controller.profilePhotoUrl!)
: null, : null,
child: controller.profilePhotoUrl == null child: controller.profilePhotoUrl == null
? Icon( ? Text(
Icons.person, controller.nama.isNotEmpty
color: Colors.blue.shade700, ? controller.nama
size: 30, .substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
fontSize: 24,
),
) )
: null, : null,
), ),
@ -417,12 +472,6 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
} }
Widget _buildPenerimaanSummary() { Widget _buildPenerimaanSummary() {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
double totalUang = 0; double totalUang = 0;
Map<String, double> totalNonUang = {}; Map<String, double> totalNonUang = {};
@ -494,7 +543,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
icon: Icons.attach_money, icon: Icons.attach_money,
color: Colors.green, color: Colors.green,
title: 'Total Bantuan Uang', title: 'Total Bantuan Uang',
value: currencyFormat.format(totalUang), value: FormatHelper.formatRupiah(totalUang),
), ),
if (totalNonUang.isNotEmpty) ...[ if (totalNonUang.isNotEmpty) ...[
if (totalUang > 0) if (totalUang > 0)

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -131,17 +131,11 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
} }
Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) { Widget _buildHeaderSection(PenerimaPenyaluranModel penyaluran) {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
// Format jumlah bantuan berdasarkan tipe (uang atau bukan) // Format jumlah bantuan berdasarkan tipe (uang atau bukan)
String formattedJumlah = ''; String formattedJumlah = '';
if (penyaluran.jumlahBantuan != null) { if (penyaluran.jumlahBantuan != null) {
if (penyaluran.isUang == true) { if (penyaluran.isUang == true) {
formattedJumlah = currencyFormat.format(penyaluran.jumlahBantuan); formattedJumlah = FormatHelper.formatRupiah(penyaluran.jumlahBantuan);
} else { } else {
formattedJumlah = formattedJumlah =
'${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}'; '${penyaluran.jumlahBantuan} ${penyaluran.satuan ?? ''}';
@ -390,8 +384,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.calendar_today, icon: Icons.calendar_today,
title: 'Tanggal Penerimaan', title: 'Tanggal Penerimaan',
value: penyaluran.tanggalPenerimaan != null value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
.format(penyaluran.tanggalPenerimaan!)
: 'Belum diterima', : 'Belum diterima',
statusColor: null, statusColor: null,
), ),
@ -400,8 +393,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.access_time, icon: Icons.access_time,
title: 'Waktu Penerimaan', title: 'Waktu Penerimaan',
value: penyaluran.tanggalPenerimaan != null value: penyaluran.tanggalPenerimaan != null
? DateFormat('HH:mm', 'id_ID') ? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
.format(penyaluran.tanggalPenerimaan!)
: 'Belum diterima', : 'Belum diterima',
statusColor: null, statusColor: null,
), ),
@ -758,8 +750,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
icon: Icons.update, icon: Icons.update,
title: 'Terakhir Diperbarui', title: 'Terakhir Diperbarui',
value: penyaluran.tanggalPenerimaan != null value: penyaluran.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID') ? FormatHelper.formatDateTime(penyaluran.tanggalPenerimaan!)
.format(penyaluran.tanggalPenerimaan!)
: 'Tidak tersedia', : 'Tidak tersedia',
statusColor: null, statusColor: null,
), ),
@ -1394,7 +1385,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( const Text(
'Pengaduan Terdaftar', 'Pengaduan',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -1547,8 +1538,7 @@ class WargaDetailPenerimaanView extends GetView<WargaDashboardController> {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
pengaduan.tanggalPengaduan != null pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy HH:mm', 'id_ID') ? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
.format(pengaduan.tanggalPengaduan!)
: 'Tanggal tidak tersedia', : 'Tanggal tidak tersedia',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,

View File

@ -1,12 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart';
import 'package:penyaluran_app/app/modules/warga/views/form_pengaduan_view.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
class WargaPengaduanView extends GetView<WargaDashboardController> { class WargaPengaduanView extends GetView<WargaDashboardController> {
const WargaPengaduanView({super.key}); const WargaPengaduanView({super.key});
@ -380,7 +375,7 @@ class WargaPengaduanView extends GetView<WargaDashboardController> {
Expanded( Expanded(
child: Text( child: Text(
item.tanggalPengaduan != null item.tanggalPengaduan != null
? DateTimeHelper.formatDateTime( ? FormatHelper.formatDateTime(
item.tanggalPengaduan!) item.tanggalPengaduan!)
: '-', : '-',
style: TextStyle( style: TextStyle(

View File

@ -20,13 +20,13 @@ class WargaView extends GetView<WargaDashboardController> {
title: Obx(() { title: Obx(() {
switch (controller.activeTabIndex.value) { switch (controller.activeTabIndex.value) {
case 0: case 0:
return const Text('Dashboard Warga'); return const Text('Dashboard');
case 1: case 1:
return const Text('Penerimaan Bantuan'); return const Text('Penerimaan Bantuan');
case 2: case 2:
return const Text('Pengaduan'); return const Text('Pengaduan');
default: default:
return const Text('Dashboard Warga'); return const Text('Dashboard');
} }
}), }),
leading: IconButton( leading: IconButton(
@ -164,16 +164,19 @@ class WargaView extends GetView<WargaDashboardController> {
child: CircleAvatar( child: CircleAvatar(
radius: 40, radius: 40,
backgroundColor: Colors.white70, backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null && backgroundImage: controller.fotoProfil.value.isNotEmpty
controller.profilePhotoUrl!.isNotEmpty ? NetworkImage(controller.fotoProfil.value)
? NetworkImage(controller.profilePhotoUrl!)
: null, : null,
child: controller.profilePhotoUrl == null || child: controller.fotoProfil.isEmpty
controller.profilePhotoUrl!.isEmpty ? Text(
? Icon( controller.nama.isNotEmpty
Icons.person, ? controller.nama.substring(0, 1).toUpperCase()
color: Colors.white, : '?',
size: 40, style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
) )
: null, : null,
), ),
@ -292,6 +295,15 @@ class WargaView extends GetView<WargaDashboardController> {
controller.refreshData(); controller.refreshData();
}, },
), ),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem( _buildMenuItem(
icon: Icons.logout, icon: Icons.logout,
title: 'Keluar', title: 'Keluar',
@ -307,7 +319,7 @@ class WargaView extends GetView<WargaDashboardController> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text( child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan', '© ${DateTime.now().year} DisalurKita',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey, color: Colors.grey,

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/auth/views/forgot_password_view.dart';
import 'package:penyaluran_app/app/modules/auth/views/login_view.dart'; import 'package:penyaluran_app/app/modules/auth/views/login_view.dart';
import 'package:penyaluran_app/app/modules/auth/views/register_donatur_view.dart'; import 'package:penyaluran_app/app/modules/auth/views/register_donatur_view.dart';
import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart'; import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart';
@ -11,6 +12,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penitipan_
import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/daftar_donatur_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_donatur_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_lokasi_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_penyaluran_page.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penyaluran_binding.dart';
@ -18,7 +20,8 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_pengaduan_
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/riwayat_pengaduan_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart';
import 'package:penyaluran_app/app/modules/about/views/about_view.dart';
import 'package:penyaluran_app/app/modules/about/bindings/about_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/donatur_binding.dart';
import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart'; import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart';
@ -64,6 +67,11 @@ class AppPages {
page: () => const RegisterDonaturView(), page: () => const RegisterDonaturView(),
binding: AuthBinding(), binding: AuthBinding(),
), ),
GetPage(
name: _Paths.forgotPassword,
page: () => const ForgotPasswordView(),
binding: AuthBinding(),
),
GetPage( GetPage(
name: Routes.wargaDashboard, name: Routes.wargaDashboard,
page: () => WargaView(), page: () => WargaView(),
@ -92,6 +100,11 @@ class AppPages {
page: () => const PetugasDesaView(), page: () => const PetugasDesaView(),
binding: PetugasDesaBinding(), binding: PetugasDesaBinding(),
), ),
GetPage(
name: _Paths.about,
page: () => const AboutView(),
binding: AboutBinding(),
),
GetPage( GetPage(
name: _Paths.permintaanPenjadwalan, name: _Paths.permintaanPenjadwalan,
page: () => const PermintaanPenjadwalanView(), page: () => const PermintaanPenjadwalanView(),
@ -137,6 +150,11 @@ class AppPages {
page: () => const TambahPenyaluranView(), page: () => const TambahPenyaluranView(),
binding: PetugasDesaBinding(), binding: PetugasDesaBinding(),
), ),
GetPage(
name: _Paths.tambahLokasiPenyaluran,
page: () => const TambahLokasiPenyaluranView(),
binding: PetugasDesaBinding(),
),
GetPage( GetPage(
name: _Paths.detailPenyaluran, name: _Paths.detailPenyaluran,
page: () => DetailPenyaluranPage(), page: () => DetailPenyaluranPage(),

View File

@ -6,6 +6,7 @@ abstract class Routes {
static const login = _Paths.login; static const login = _Paths.login;
static const register = _Paths.register; static const register = _Paths.register;
static const registerDonatur = _Paths.registerDonatur; static const registerDonatur = _Paths.registerDonatur;
static const forgotPassword = _Paths.forgotPassword;
static const wargaDashboard = _Paths.wargaDashboard; static const wargaDashboard = _Paths.wargaDashboard;
static const wargaPenerimaan = _Paths.wargaPenerimaan; static const wargaPenerimaan = _Paths.wargaPenerimaan;
static const wargaPengaduan = _Paths.wargaPengaduan; static const wargaPengaduan = _Paths.wargaPengaduan;
@ -23,10 +24,12 @@ abstract class Routes {
static const konfirmasiPenerima = _Paths.konfirmasiPenerima; static const konfirmasiPenerima = _Paths.konfirmasiPenerima;
static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran; static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran;
static const profile = _Paths.profile; static const profile = _Paths.profile;
static const about = _Paths.about;
static const riwayatPenitipan = _Paths.riwayatPenitipan; static const riwayatPenitipan = _Paths.riwayatPenitipan;
static const daftarDonatur = _Paths.daftarDonatur; static const daftarDonatur = _Paths.daftarDonatur;
static const detailDonatur = _Paths.detailDonatur; static const detailDonatur = _Paths.detailDonatur;
static const tambahPenyaluran = _Paths.tambahPenyaluran; static const tambahPenyaluran = _Paths.tambahPenyaluran;
static const tambahLokasiPenyaluran = _Paths.tambahLokasiPenyaluran;
static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran; static const daftarPenerimaPenyaluran = _Paths.daftarPenerimaPenyaluran;
static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran; static const detailPenerimaPenyaluran = _Paths.detailPenerimaPenyaluran;
static const laporanPenyaluran = _Paths.laporanPenyaluran; static const laporanPenyaluran = _Paths.laporanPenyaluran;
@ -51,6 +54,7 @@ abstract class _Paths {
static const login = '/login'; static const login = '/login';
static const register = '/register'; static const register = '/register';
static const registerDonatur = '/register-donatur'; static const registerDonatur = '/register-donatur';
static const forgotPassword = '/forgot-password';
static const wargaDashboard = '/warga-dashboard'; static const wargaDashboard = '/warga-dashboard';
static const wargaPenerimaan = '/warga-penerimaan'; static const wargaPenerimaan = '/warga-penerimaan';
static const wargaPengaduan = '/warga-pengaduan'; static const wargaPengaduan = '/warga-pengaduan';
@ -68,10 +72,12 @@ abstract class _Paths {
static const konfirmasiPenerima = '/daftar-penerima/konfirmasi'; static const konfirmasiPenerima = '/daftar-penerima/konfirmasi';
static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran'; static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran';
static const profile = '/profile'; static const profile = '/profile';
static const about = '/about';
static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan'; static const riwayatPenitipan = '/petugas-desa/riwayat-penitipan';
static const daftarDonatur = '/daftar-donatur'; static const daftarDonatur = '/daftar-donatur';
static const detailDonatur = '/daftar-donatur/detail'; static const detailDonatur = '/daftar-donatur/detail';
static const tambahPenyaluran = '/tambah-penyaluran'; static const tambahPenyaluran = '/tambah-penyaluran';
static const tambahLokasiPenyaluran = '/tambah-lokasi-penyaluran';
static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran'; static const daftarPenerimaPenyaluran = '/daftar-penerima-penyaluran';
static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran'; static const detailPenerimaPenyaluran = '/detail-penerima-penyaluran';
static const laporanPenyaluran = '/laporan-penyaluran'; static const laporanPenyaluran = '/laporan-penyaluran';

View 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');
}
}
}

View 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;
}
}
}

View File

@ -562,19 +562,15 @@ class SupabaseService extends GetxService {
try { try {
final now = DateTime.now(); final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day); final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final week = today.add(const Duration(days: 7)); final week = today.add(const Duration(days: 7));
// Konversi ke UTC untuk query ke database
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
final weekUtc = week.toUtc().toIso8601String();
final response = await client final response = await client
.from('penyaluran_bantuan') .from('penyaluran_bantuan')
.select('*') .select('*')
.gte('tanggal_penyaluran', tomorrowUtc) .gte('tanggal_penyaluran', today)
.lt('tanggal_penyaluran', weekUtc) .lt('tanggal_penyaluran', week)
.inFilter('status', ['DIJADWALKAN']); .inFilter('status', ['DIJADWALKAN']).order('tanggal_penyaluran',
ascending: true);
return response; return response;
} catch (e) { } catch (e) {
@ -651,15 +647,128 @@ class SupabaseService extends GetxService {
} }
// Metode untuk memperbarui status jadwal // Metode untuk memperbarui status jadwal
Future<void> updateJadwalStatus(String jadwalId, String status) async { Future<void> updateJadwalStatus(String jadwalId, String newStatus) async {
try { try {
await client.from('penyaluran_bantuan').update({ await client.from('penyaluran_bantuan').update({
'status': status, 'status': newStatus,
'updated_at': DateTime.now().toUtc().toIso8601String(), 'updated_at': DateTime.now().toUtc().toIso8601String()
}).eq('id', jadwalId); }).eq('id', jadwalId);
print('Jadwal status updated: $jadwalId -> $newStatus');
} catch (e) { } catch (e) {
print('Error updating jadwal status: $e'); print('Error updating jadwal status: $e');
throw e.toString(); rethrow;
}
}
// Update status jadwal penyaluran secara batch untuk efisiensi
Future<void> batchUpdateJadwalStatus(
Map<String, String> jadwalUpdates) async {
if (jadwalUpdates.isEmpty) return;
try {
print('Attempting batch update for ${jadwalUpdates.length} jadwal');
final timestamp = DateTime.now().toUtc().toIso8601String();
// Format data sesuai dengan yang diharapkan oleh SQL function
final List<Map<String, dynamic>> formattedUpdates = jadwalUpdates.entries
.map((e) => {'id': e.key, 'status': e.value})
.toList();
print('Formatted updates: $formattedUpdates');
try {
// Coba gunakan RPC dulu - kirim sebagai array dari objek JSON
final result = await client.rpc('batch_update_jadwal_status', params: {
'jadwal_updates': formattedUpdates,
'updated_timestamp': timestamp,
});
print('Batch update via RPC response: $result');
// Periksa hasil untuk mengkonfirmasi berapa banyak yang berhasil diupdate
if (result != null) {
final bool success = result['success'] == true;
final int updatedCount = result['updated_count'] ?? 0;
if (success) {
print('Successfully updated $updatedCount records via RPC');
// Log ID yang berhasil diupdate
final List<String> successIds =
List<String>.from(result['success_ids'] ?? []);
if (successIds.isNotEmpty) {
print(
'Successfully updated jadwal IDs: ${successIds.join(", ")}');
}
// Jika ada yang gagal, log untuk debugging
if (updatedCount < jadwalUpdates.length) {
print(
'Warning: ${jadwalUpdates.length - updatedCount} records failed to update');
// Periksa apakah ada informasi error
if (result['errors'] != null) {
final int errorCount = result['errors']['count'] ?? 0;
if (errorCount > 0) {
final List<String> errorIds =
List<String>.from(result['errors']['ids'] ?? []);
final List<String> errorMessages =
List<String>.from(result['errors']['messages'] ?? []);
for (int i = 0; i < errorCount; i++) {
if (i < errorIds.length && i < errorMessages.length) {
print(
'Error updating jadwal ${errorIds[i]}: ${errorMessages[i]}');
}
}
}
}
// Update individual yang gagal menggunakan metode satu per satu
for (var entry in jadwalUpdates.entries) {
if (!successIds.contains(entry.key)) {
try {
await updateJadwalStatus(entry.key, entry.value);
print('Fallback update successful for jadwal ${entry.key}');
} catch (e) {
print(
'Fallback update also failed for jadwal ${entry.key}: $e');
}
}
}
}
} else {
print(
'Batch update reported failure. Falling back to individual updates.');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} else {
print(
'Batch update returned null result. Falling back to individual updates.');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} catch (rpcError) {
print('RPC batch update failed: $rpcError');
print('Falling back to individual updates');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} catch (e) {
print('Error in batch update process: $e');
rethrow;
}
}
// Helper function untuk fallback ke individual updates
Future<void> _fallbackToIndividualUpdates(
Map<String, String> jadwalUpdates) async {
for (var entry in jadwalUpdates.entries) {
try {
await updateJadwalStatus(entry.key, entry.value);
print('Individual update successful: ${entry.key} -> ${entry.value}');
} catch (updateError) {
print('Failed to update jadwal ${entry.key}: $updateError');
}
} }
} }
@ -874,7 +983,7 @@ class SupabaseService extends GetxService {
.select('stok_bantuan_id, jumlah') .select('stok_bantuan_id, jumlah')
.eq('id', penitipanId); .eq('id', penitipanId);
if (response == null || response.isEmpty) { if (response.isEmpty) {
throw 'Data penitipan tidak ditemukan'; throw 'Data penitipan tidak ditemukan';
} }
@ -1930,8 +2039,8 @@ class SupabaseService extends GetxService {
} }
if (jenisPerubahan != null) { if (jenisPerubahan != null) {
filterString += (filterString.isNotEmpty ? ',' : '') + filterString +=
'jenis_perubahan.eq.$jenisPerubahan'; '${filterString.isNotEmpty ? ',' : ''}jenis_perubahan.eq.$jenisPerubahan';
} }
final response = await client.from('riwayat_stok').select(''' final response = await client.from('riwayat_stok').select('''
@ -2006,7 +2115,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil ditambahkan dari penitipan'); print('Stok berhasil ditambahkan dari penitipan');
} catch (e) { } catch (e) {
print('Error adding stok from penitipan: $e'); print('Error adding stok from penitipan: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
} }
} }
@ -2058,7 +2167,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil dikurangi dari penyaluran'); print('Stok berhasil dikurangi dari penyaluran');
} catch (e) { } catch (e) {
print('Error reducing stok from penyaluran: $e'); print('Error reducing stok from penyaluran: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
} }
} }
@ -2075,7 +2184,7 @@ class SupabaseService extends GetxService {
String fotoBuktiUrl = ''; String fotoBuktiUrl = '';
if (fotoBuktiPath.isNotEmpty) { if (fotoBuktiPath.isNotEmpty) {
final String fileName = final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg'; '${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
final fileResponse = await client.storage.from('stok_bukti').upload( final fileResponse = await client.storage.from('stok_bukti').upload(
fileName, fileName,
File(fotoBuktiPath), File(fotoBuktiPath),
@ -2125,7 +2234,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil ditambahkan secara manual'); print('Stok berhasil ditambahkan secara manual');
} catch (e) { } catch (e) {
print('Error adding stok manually: $e'); print('Error adding stok manually: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
} }
} }
@ -2164,7 +2273,7 @@ class SupabaseService extends GetxService {
String fotoBuktiUrl = ''; String fotoBuktiUrl = '';
if (fotoBuktiPath.isNotEmpty) { if (fotoBuktiPath.isNotEmpty) {
final String fileName = final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg'; '${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
final fileResponse = await client.storage.from('stok_bukti').upload( final fileResponse = await client.storage.from('stok_bukti').upload(
fileName, fileName,
File(fotoBuktiPath), File(fotoBuktiPath),
@ -2198,7 +2307,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil dikurangi secara manual'); print('Stok berhasil dikurangi secara manual');
} catch (e) { } catch (e) {
print('Error reducing stok manually: $e'); print('Error reducing stok manually: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
} }
} }

View File

@ -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]}.',
)}';
}
}
}

View File

@ -4,7 +4,7 @@ import 'package:intl/intl.dart';
/// ///
/// Kelas ini berisi fungsi-fungsi untuk memformat dan memanipulasi /// Kelas ini berisi fungsi-fungsi untuk memformat dan memanipulasi
/// tanggal dan waktu. /// tanggal dan waktu.
class DateTimeHelper { class FormatHelper {
/// Mengkonversi DateTime dari UTC ke timezone lokal /// Mengkonversi DateTime dari UTC ke timezone lokal
static DateTime toLocalDateTime(DateTime utcDateTime) { static DateTime toLocalDateTime(DateTime utcDateTime) {
return utcDateTime.toLocal(); return utcDateTime.toLocal();
@ -70,7 +70,6 @@ class DateTimeHelper {
static String formatDateTime( static String formatDateTime(
DateTime? dateTime, { DateTime? dateTime, {
String format = 'dd MMM yyyy HH:mm', String format = 'dd MMM yyyy HH:mm',
String locale = 'id_ID',
String defaultValue = 'Belum ditentukan', String defaultValue = 'Belum ditentukan',
}) { }) {
if (dateTime == null) return defaultValue; if (dateTime == null) return defaultValue;
@ -78,7 +77,7 @@ class DateTimeHelper {
// Pastikan tanggal dan waktu dalam timezone lokal // Pastikan tanggal dan waktu dalam timezone lokal
final localDateTime = toLocalDateTime(dateTime); final localDateTime = toLocalDateTime(dateTime);
try { try {
return DateFormat(format, locale).format(localDateTime); return DateFormat(format).format(localDateTime);
} catch (e) { } catch (e) {
print('Error formatting date time: $e'); print('Error formatting date time: $e');
return localDateTime.toString(); // Fallback to basic format return localDateTime.toString(); // Fallback to basic format
@ -197,8 +196,10 @@ class DateTimeHelper {
final String tanggal = localDateTime.day.toString().padLeft(2, '0'); final String tanggal = localDateTime.day.toString().padLeft(2, '0');
final String bulan = namaBulan[localDateTime.month - 1]; final String bulan = namaBulan[localDateTime.month - 1];
final String tahun = localDateTime.year.toString(); final String tahun = localDateTime.year.toString();
final String jam = localDateTime.hour.toString().padLeft(2, '0');
final String menit = localDateTime.minute.toString().padLeft(2, '0');
return '$hari, $tanggal $bulan $tahun'; return '$hari, $tanggal $bulan $tahun $jam:$menit';
} }
/// Format angka dengan pemisah ribuan /// Format angka dengan pemisah ribuan

View File

@ -38,14 +38,20 @@ class AppDrawer extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundColor: Colors.white, backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
backgroundImage: backgroundImage: avatar != null && avatar!.isNotEmpty
avatar != null ? NetworkImage(avatar!) : null, ? NetworkImage(avatar!)
child: avatar == null : null,
? const Icon( child: (avatar == null || avatar!.isEmpty)
Icons.person, ? Text(
size: 40, nama.isNotEmpty
color: AppTheme.primaryColor, ? nama.substring(0, 1).toUpperCase()
: '?',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
) )
: null, : null,
), ),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart'; import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/status_badge.dart'; import 'package:penyaluran_app/app/widgets/status_badge.dart';
class BantuanCard extends StatelessWidget { class BantuanCard extends StatelessWidget {
@ -17,17 +17,11 @@ class BantuanCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
// Format jumlah bantuan berdasarkan tipe (uang atau bukan) // Format jumlah bantuan berdasarkan tipe (uang atau bukan)
String formattedJumlah = ''; String formattedJumlah = '';
if (item.jumlahBantuan != null) { if (item.jumlahBantuan != null) {
if (item.isUang == true) { if (item.isUang == true) {
formattedJumlah = currencyFormat.format(item.jumlahBantuan); formattedJumlah = FormatHelper.formatRupiah(item.jumlahBantuan);
} else { } else {
formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}'; formattedJumlah = '${item.jumlahBantuan} ${item.satuan ?? ''}';
} }
@ -120,8 +114,8 @@ class BantuanCard extends StatelessWidget {
Flexible( Flexible(
child: Text( child: Text(
item.tanggalPenerimaan != null item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(
.format(item.tanggalPenerimaan!) item.tanggalPenerimaan!)
: '-', : '-',
style: TextStyle( style: TextStyle(
color: Colors.grey.shade600, color: Colors.grey.shade600,
@ -373,8 +367,8 @@ class BantuanCard extends StatelessWidget {
Icons.calendar_today, Icons.calendar_today,
'Tanggal:', 'Tanggal:',
item.tanggalPenerimaan != null item.tanggalPenerimaan != null
? DateFormat('dd MMMM yyyy', 'id_ID') ? FormatHelper.formatDateTime(
.format(item.tanggalPenerimaan!) item.tanggalPenerimaan!)
: '-', : '-',
), ),
const Divider(height: 16), const Divider(height: 16),

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/theme/app_colors.dart'; import 'package:penyaluran_app/app/theme/app_colors.dart';
import 'package:penyaluran_app/app/widgets/dialogs/show_image_dialog.dart';
/// Dialog untuk menampilkan detail penitipan bantuan /// Dialog untuk menampilkan detail penitipan bantuan
/// ///
@ -48,7 +49,7 @@ class DetailPenitipanDialog {
), ),
_buildInfoRow( _buildInfoRow(
'Tanggal Penitipan', 'Tanggal Penitipan',
DateTimeHelper.formatDateTime( FormatHelper.formatDateTime(
item.tanggalPenitipan ?? item.createdAt), item.tanggalPenitipan ?? item.createdAt),
), ),
_buildInfoRow( _buildInfoRow(
@ -63,7 +64,7 @@ class DetailPenitipanDialog {
if (item.tanggalVerifikasi != null) if (item.tanggalVerifikasi != null)
_buildInfoRow( _buildInfoRow(
'Tanggal Verifikasi', 'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi), FormatHelper.formatDateTime(item.tanggalVerifikasi),
), ),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty) if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
_buildInfoRow('Deskripsi', item.deskripsi!), _buildInfoRow('Deskripsi', item.deskripsi!),
@ -143,50 +144,7 @@ class DetailPenitipanDialog {
/// Menampilkan gambar dalam layar penuh /// Menampilkan gambar dalam layar penuh
static void showFullScreenImage(BuildContext context, String imageUrl) { static void showFullScreenImage(BuildContext context, String imageUrl) {
Navigator.of(context).push( ShowImageDialog.showFullScreen(context, imageUrl);
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Container(
color: Colors.black,
child: Center(
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(20),
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Text(
'Gagal memuat gambar',
style: TextStyle(color: Colors.white),
),
);
},
),
),
),
),
),
),
);
} }
/// Membangun baris informasi /// Membangun baris informasi

View 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,
),
);
}
}
}

View File

@ -14,6 +14,7 @@ export 'cards/info_card.dart';
// Dialogs // Dialogs
export 'dialogs/detail_penitipan_dialog.dart'; export 'dialogs/detail_penitipan_dialog.dart';
export 'dialogs/confirmation_dialog.dart'; export 'dialogs/confirmation_dialog.dart';
export 'dialogs/show_image_dialog.dart';
// Indicators // Indicators
export 'indicators/loading_indicator.dart'; export 'indicators/loading_indicator.dart';

View File

@ -9,6 +9,7 @@ import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:syncfusion_localizations/syncfusion_localizations.dart'; import 'package:syncfusion_localizations/syncfusion_localizations.dart';
import 'package:penyaluran_app/app/services/notification_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -27,11 +28,26 @@ void main() async {
// Inisialisasi service // Inisialisasi service
Future<void> initServices() async { Future<void> initServices() async {
await Get.putAsync(() => SupabaseService().init()); print('Initializing services...');
await Get.putAsync(() => AuthService().init()); // Inisialisasi SupabaseService dengan pendekatan async
final supabaseService =
await Get.putAsync(() => SupabaseService().init(), permanent: true);
print('SupabaseService initialized: ${supabaseService != null}');
// Inisialisasi AuthService
final authService =
await Get.putAsync(() => AuthService().init(), permanent: true);
print('AuthService initialized: ${authService != null}');
// Inisialisasi AuthController secara global // Inisialisasi AuthController secara global
Get.put(AuthController(), permanent: true); final authController = Get.put(AuthController(), permanent: true);
print('AuthController initialized: ${authController != null}');
// Register NotificationService
final notificationService = Get.put(NotificationService(), permanent: true);
print('NotificationService initialized: ${notificationService != null}');
print('All services initialized');
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -40,7 +56,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetMaterialApp( return GetMaterialApp(
title: 'Penerimaan App', title: 'DisalurKita',
theme: AppTheme.lightTheme, theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme, darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.light, // Default ke tema terang themeMode: ThemeMode.light, // Default ke tema terang
@ -60,6 +76,7 @@ class MyApp extends StatelessWidget {
Locale('id', 'ID'), // Indonesia Locale('id', 'ID'), // Indonesia
Locale('en', 'US'), // English Locale('en', 'US'), // English
], ],
// initialBinding tidak diperlukan lagi karena service sudah diinisialisasi di initServices()
); );
} }
} }

View File

@ -11,6 +11,7 @@ import file_selector_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import open_file_mac import open_file_mac
import path_provider_foundation import path_provider_foundation
import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@ -670,7 +670,7 @@ packages:
source: hosted source: hosted
version: "0.0.3" version: "0.0.3"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -837,6 +837,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.28.0" version: "0.28.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.2"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -94,6 +94,8 @@ dependencies:
uuid: ^4.5.1 uuid: ^4.5.1
# Library untuk cached image # Library untuk cached image
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
share_plus: ^10.1.4
path: ^1.9.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

BIN
temp.txt

Binary file not shown.

View File

@ -9,6 +9,7 @@
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
file_selector_windows file_selector_windows
flutter_secure_storage_windows flutter_secure_storage_windows
share_plus
url_launcher_windows url_launcher_windows
) )