Perbarui dependensi dan tambahkan fungsionalitas laporan penyaluran. Tambahkan paket baru seperti file_picker, pdf, dan open_file ke dalam pubspec.yaml. Hapus model LaporanModel yang tidak digunakan dan ganti dengan LaporanPenyaluranModel. Modifikasi tampilan dan controller untuk mendukung pengelolaan laporan penyaluran, termasuk navigasi dan ekspor ke PDF. Perbarui rute aplikasi untuk mencakup halaman laporan penyaluran baru.
This commit is contained in:
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint ٬ك<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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt ٬ك<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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json ٬ك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json ٬ك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||
n
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja ٬ك<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt ٬ك<EFBFBD>2y
|
||||
w
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt ٬ك<EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
|
||||
x
|
||||
x
|
||||
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json ٬ك<D9AC>2 ~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin ٬ك<D9AC>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt ٬ك<D9AC>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 ٬ك<D9AC>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><>ك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <08><><EFBFBD><EFBFBD><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>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json <08><><EFBFBD><EFBFBD><EFBFBD>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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json íك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2r
|
||||
p
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
nD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja íك<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build.ninja.txt íك<EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\build_file_index.txt íك<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 íك<C3AD>2 <09>
|
||||
~
|
||||
~
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin íك<C3AD>2
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt íك<C3AD>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 íك<C3AD>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><>ك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\additional_project_files.txt <08><><EFBFBD><EFBFBD><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>2 <20><><EFBFBD><EFBFBD><EFBFBD>2x
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build.json <08><><EFBFBD><EFBFBD><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>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2}
|
||||
{
|
||||
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
|
||||
yD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\android_gradle_build_mini.json <08><>ك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2j
|
||||
h
|
||||
fD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja <08><><EFBFBD><EFBFBD><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>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2n
|
||||
l
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08><><EFBFBD><EFBFBD><EFBFBD>2s
|
||||
jD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build.ninja.txt <08><>ك<EFBFBD>2s
|
||||
q
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08><><EFBFBD><EFBFBD><EFBFBD>2
|
||||
oD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\build_file_index.txt <08><>ك<EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||
r
|
||||
r
|
||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json <08><>ك<EFBFBD>2 x
|
||||
v
|
||||
v
|
||||
tD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\compile_commands.json.bin <08><>ك<EFBFBD>2
|
||||
~
|
||||
|
|
||||
|
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86\metadata_generation_command.txt <08><>ك<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>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2v
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
||||
M
|
||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||
A
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint ܯك<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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\additional_project_files.txt ܯك<EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2{
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build.json ܯك<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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\android_gradle_build_mini.json ܯك<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2m
|
||||
k
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
iD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja ݯك<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2q
|
||||
o
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2v
|
||||
mD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build.ninja.txt ݯك<EFBFBD>2v
|
||||
t
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2
|
||||
rD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\build_file_index.txt ݯك<EFBFBD>2
|
||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||
u
|
||||
u
|
||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json ݯك<DDAF>2 {
|
||||
y
|
||||
y
|
||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin ݯك<DDAF>2
|
||||
<EFBFBD>
|
||||
|
||||
|
||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt ݯك<DDAF>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 ݯك<DDAF>2
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
BIN
assets/font/DMSans-Bold.ttf
Normal file
BIN
assets/font/DMSans-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/font/DMSans-Regular.ttf
Normal file
BIN
assets/font/DMSans-Regular.ttf
Normal file
Binary file not shown.
@ -1,89 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class LaporanModel {
|
||||
final String? id;
|
||||
final String? judul;
|
||||
final String? deskripsi;
|
||||
final String? jenis; // Contoh: 'PENYALURAN', 'STOK_BANTUAN', 'PENERIMA'
|
||||
final String?
|
||||
referensiId; // ID dari entitas yang dilaporkan (penyaluran, penitipan, dll)
|
||||
final String? status; // Contoh: 'draft', 'final', 'disetujui'
|
||||
final String? petugasId; // Pengguna yang membuat laporan
|
||||
final List<String>? fileUrls; // URL file laporan (PDF, Excel, dll)
|
||||
final DateTime? tanggalMulai;
|
||||
final DateTime? tanggalSelesai;
|
||||
final DateTime? tanggalLaporan;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
LaporanModel({
|
||||
this.id,
|
||||
this.judul,
|
||||
this.deskripsi,
|
||||
this.jenis,
|
||||
this.referensiId,
|
||||
this.status,
|
||||
this.petugasId,
|
||||
this.fileUrls,
|
||||
this.tanggalMulai,
|
||||
this.tanggalSelesai,
|
||||
this.tanggalLaporan,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory LaporanModel.fromRawJson(String str) =>
|
||||
LaporanModel.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory LaporanModel.fromJson(Map<String, dynamic> json) => LaporanModel(
|
||||
id: json["id"],
|
||||
judul: json["judul"],
|
||||
deskripsi: json["deskripsi"],
|
||||
jenis: json["jenis"],
|
||||
referensiId: json["referensi_id"],
|
||||
status: json["status"],
|
||||
petugasId: json["petugas_id"] ?? json["user_id"],
|
||||
fileUrls: json["file_urls"] == null
|
||||
? null
|
||||
: List<String>.from(json["file_urls"].map((x) => x)),
|
||||
tanggalMulai: json["tanggal_mulai"] != null
|
||||
? DateTime.parse(json["tanggal_mulai"])
|
||||
: json["periode_awal"] != null
|
||||
? DateTime.parse(json["periode_awal"])
|
||||
: null,
|
||||
tanggalSelesai: json["tanggal_selesai"] != null
|
||||
? DateTime.parse(json["tanggal_selesai"])
|
||||
: json["periode_akhir"] != null
|
||||
? DateTime.parse(json["periode_akhir"])
|
||||
: null,
|
||||
tanggalLaporan: json["tanggal_laporan"] != null
|
||||
? DateTime.parse(json["tanggal_laporan"])
|
||||
: null,
|
||||
createdAt: json["created_at"] != null
|
||||
? DateTime.parse(json["created_at"])
|
||||
: null,
|
||||
updatedAt: json["updated_at"] != null
|
||||
? DateTime.parse(json["updated_at"])
|
||||
: null,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"judul": judul,
|
||||
"deskripsi": deskripsi,
|
||||
"jenis": jenis,
|
||||
"referensi_id": referensiId,
|
||||
"status": status,
|
||||
"petugas_id": petugasId,
|
||||
"file_urls": fileUrls == null
|
||||
? null
|
||||
: List<dynamic>.from(fileUrls!.map((x) => x)),
|
||||
"tanggal_mulai": tanggalMulai?.toIso8601String(),
|
||||
"tanggal_selesai": tanggalSelesai?.toIso8601String(),
|
||||
"tanggal_laporan": tanggalLaporan?.toIso8601String(),
|
||||
"created_at": createdAt?.toIso8601String(),
|
||||
"updated_at": updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
61
lib/app/data/models/laporan_penyaluran_model.dart
Normal file
61
lib/app/data/models/laporan_penyaluran_model.dart
Normal file
@ -0,0 +1,61 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class LaporanPenyaluranModel {
|
||||
final String? id;
|
||||
final String penyaluranBantuanId;
|
||||
final String judul;
|
||||
final String? dokumentasiUrl;
|
||||
final String? beritaAcaraUrl;
|
||||
final DateTime? tanggalLaporan;
|
||||
final String? status;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
LaporanPenyaluranModel({
|
||||
this.id,
|
||||
required this.penyaluranBantuanId,
|
||||
required this.judul,
|
||||
this.dokumentasiUrl,
|
||||
this.beritaAcaraUrl,
|
||||
this.tanggalLaporan,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory LaporanPenyaluranModel.fromRawJson(String str) =>
|
||||
LaporanPenyaluranModel.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory LaporanPenyaluranModel.fromJson(Map<String, dynamic> json) =>
|
||||
LaporanPenyaluranModel(
|
||||
id: json["id"],
|
||||
penyaluranBantuanId: json["penyaluran_bantuan_id"],
|
||||
judul: json["judul"],
|
||||
dokumentasiUrl: json["dokumentasi_url"],
|
||||
beritaAcaraUrl: json["berita_acara_url"],
|
||||
tanggalLaporan: json["tanggal_laporan"] != null
|
||||
? DateTime.parse(json["tanggal_laporan"]).toUtc()
|
||||
: null,
|
||||
status: json["status"],
|
||||
createdAt: json["created_at"] != null
|
||||
? DateTime.parse(json["created_at"]).toUtc()
|
||||
: null,
|
||||
updatedAt: json["updated_at"] != null
|
||||
? DateTime.parse(json["updated_at"]).toUtc()
|
||||
: null,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"penyaluran_bantuan_id": penyaluranBantuanId,
|
||||
"judul": judul,
|
||||
"dokumentasi_url": dokumentasiUrl,
|
||||
"berita_acara_url": beritaAcaraUrl,
|
||||
"tanggal_laporan": tanggalLaporan?.toUtc().toIso8601String(),
|
||||
"status": status,
|
||||
"created_at": createdAt?.toUtc().toIso8601String(),
|
||||
"updated_at": updatedAt?.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
|
||||
class LaporanPenyaluranBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<LaporanPenyaluranController>(
|
||||
() => LaporanPenyaluranController(),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,623 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class LaporanPenyaluranCreateView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranCreateView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final penyaluranId = Get.arguments as String;
|
||||
|
||||
// Dapatkan info penyaluran
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchPenyaluranDetail(penyaluranId);
|
||||
controller.resetForm(); // Reset form setiap kali halaman dibuka
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Buat Laporan Penyaluran',
|
||||
// subtitle: 'Isi form untuk membuat laporan penyaluran',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informasi penyaluran
|
||||
if (controller.selectedPenyaluran.value != null) ...[
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Informasi Penyaluran',
|
||||
// subtitle: 'Penyaluran yang akan dibuatkan laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(
|
||||
'Nama Penyaluran',
|
||||
controller.selectedPenyaluran.value!.nama ?? '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Penyaluran',
|
||||
controller.selectedPenyaluran.value!
|
||||
.tanggalPenyaluran !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
.selectedPenyaluran.value!.tanggalPenyaluran!)
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Selesai',
|
||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||
null
|
||||
? DateTimeHelper.formatDateTime(controller
|
||||
.selectedPenyaluran.value!.tanggalSelesai!)
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Jumlah Penerima',
|
||||
'${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Form laporan
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Form Laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Judul laporan
|
||||
_buildTextField(
|
||||
controller: controller.judulController,
|
||||
label: 'Judul Laporan',
|
||||
hint: 'Masukkan judul laporan',
|
||||
required: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Dokumentasi dan Berita Acara
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Dokumentasi & Berita Acara',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Upload Dokumentasi
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => controller.dokumentasiPath.isNotEmpty
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
File(controller
|
||||
.dokumentasiPath.value),
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller
|
||||
.dokumentasiPath.value = '';
|
||||
},
|
||||
icon: Icon(Icons.delete,
|
||||
color: AppTheme.errorColor),
|
||||
label: Text('Hapus',
|
||||
style: TextStyle(
|
||||
color:
|
||||
AppTheme.errorColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label:
|
||||
const Text('Upload Foto Dokumentasi'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format gambar: JPG, PNG, JPEG (maks. 5MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Upload Berita Acara
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Berita Acara',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => controller.beritaAcaraPath.isNotEmpty
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor
|
||||
.withOpacity(0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.description,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dokumen Berita Acara',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
controller.beritaAcaraPath.value
|
||||
.split('/')
|
||||
.last,
|
||||
style: const TextStyle(
|
||||
fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.beritaAcaraPath.value =
|
||||
'';
|
||||
},
|
||||
icon: Icon(Icons.delete,
|
||||
color: AppTheme.errorColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.file_present),
|
||||
label: const Text(
|
||||
'Upload Dokumen Berita Acara'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format file: PDF, DOC, DOCX (maks. 10MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Tombol simpan
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isSaving.value
|
||||
? null
|
||||
: () {
|
||||
if (controller.judulController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Judul laporan wajib diisi',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
controller.saveLaporan(penyaluranId);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: controller.isSaving.value
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Simpan Laporan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk memilih gambar dokumentasi
|
||||
void _pickDocumentationImage(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
context,
|
||||
Icons.camera_alt,
|
||||
'Kamera',
|
||||
ImageSource.camera,
|
||||
Colors.blue,
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
context,
|
||||
Icons.photo_library,
|
||||
'Galeri',
|
||||
ImageSource.gallery,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk opsi sumber gambar
|
||||
Widget _buildImageSourceOption(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
ImageSource source,
|
||||
Color color,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final ImagePicker picker = ImagePicker();
|
||||
try {
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: source,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (image != null) {
|
||||
controller.dokumentasiPath.value = image.path;
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih gambar: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 48, color: color),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk memilih file berita acara
|
||||
Future<void> _pickBeritaAcaraFile(BuildContext context) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'doc', 'docx'],
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
controller.beritaAcaraPath.value = result.files.single.path!;
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memilih file: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget untuk item informasi
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk input teks
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
bool required = false,
|
||||
int maxLines = 1,
|
||||
bool isReadOnly = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
children: required
|
||||
? [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(
|
||||
color: AppTheme.errorColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
readOnly: isReadOnly,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: AppTheme.primaryColor, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,700 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class LaporanPenyaluranEditView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranEditView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final laporanId = Get.arguments as String;
|
||||
|
||||
// Dapatkan data laporan
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchLaporanDetail(laporanId).then((_) {
|
||||
if (controller.selectedLaporan.value != null) {
|
||||
controller.setFormForEdit(controller.selectedLaporan.value!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Edit Laporan Penyaluran',
|
||||
// subtitle: 'Perbarui informasi laporan penyaluran',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.selectedLaporan.value == null) {
|
||||
return const Center(
|
||||
child: Text('Laporan tidak ditemukan'),
|
||||
);
|
||||
}
|
||||
|
||||
// Cek status laporan
|
||||
if (controller.selectedLaporan.value!.status == 'FINAL') {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lock,
|
||||
size: 64,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Laporan Telah Difinalisasi',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Laporan yang sudah difinalisasi tidak dapat diedit lagi.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Kembali ke Detail Laporan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informasi penyaluran
|
||||
if (controller.selectedPenyaluran.value != null) ...[
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Informasi Penyaluran',
|
||||
// subtitle: 'Penyaluran yang terkait dengan laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(
|
||||
'Nama Penyaluran',
|
||||
controller.selectedPenyaluran.value!.nama ?? '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Penyaluran',
|
||||
controller.selectedPenyaluran.value!
|
||||
.tanggalPenyaluran !=
|
||||
null
|
||||
? '${controller.selectedPenyaluran.value!.tanggalPenyaluran!.day}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.month}/${controller.selectedPenyaluran.value!.tanggalPenyaluran!.year}'
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Tanggal Selesai',
|
||||
controller.selectedPenyaluran.value!.tanggalSelesai !=
|
||||
null
|
||||
? '${controller.selectedPenyaluran.value!.tanggalSelesai!.day}/${controller.selectedPenyaluran.value!.tanggalSelesai!.month}/${controller.selectedPenyaluran.value!.tanggalSelesai!.year}'
|
||||
: '-',
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Jumlah Penerima',
|
||||
'${controller.selectedPenyaluran.value!.jumlahPenerima ?? 0} orang',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Form laporan
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Form Laporan',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Judul laporan
|
||||
_buildTextField(
|
||||
controller: controller.judulController,
|
||||
label: 'Judul Laporan',
|
||||
hint: 'Masukkan judul laporan',
|
||||
required: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Dokumentasi dan Berita Acara
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Dokumentasi & Berita Acara',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Upload Dokumentasi
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Dokumentasi',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Dokumentasi yang sudah ada
|
||||
if (controller
|
||||
.selectedLaporan.value?.dokumentasiUrl !=
|
||||
null &&
|
||||
controller.dokumentasiPath.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.selectedLaporan.value!
|
||||
.dokumentasiUrl!,
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child:
|
||||
Text('Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.edit,
|
||||
color: Colors.blue),
|
||||
label: const Text('Ganti',
|
||||
style:
|
||||
TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Dokumentasi yang baru dipilih
|
||||
else if (controller.dokumentasiPath.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(
|
||||
File(controller.dokumentasiPath.value),
|
||||
width: 200,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
Container(
|
||||
width: 200,
|
||||
height: 120,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Center(
|
||||
child:
|
||||
Text('Pratinjau tidak tersedia'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.dokumentasiPath.value = '';
|
||||
},
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
label: const Text('Hapus',
|
||||
style:
|
||||
TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Tidak ada dokumentasi
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _pickDocumentationImage(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Upload Foto Dokumentasi'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(color: Colors.blue.shade300),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format gambar: JPG, PNG, JPEG (maks. 5MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Upload Berita Acara
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: 'Berita Acara',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Berita acara yang sudah ada
|
||||
if (controller
|
||||
.selectedLaporan.value?.beritaAcaraUrl !=
|
||||
null &&
|
||||
controller.beritaAcaraPath.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.description,
|
||||
size: 40,
|
||||
color: Colors.blue.shade700),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Dokumen Berita Acara',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
Uri.parse(controller
|
||||
.selectedLaporan
|
||||
.value!
|
||||
.beritaAcaraUrl!)
|
||||
.pathSegments
|
||||
.last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.edit,
|
||||
color: Colors.blue),
|
||||
label: const Text('Ganti',
|
||||
style:
|
||||
TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Berita acara yang baru dipilih
|
||||
else if (controller.beritaAcaraPath.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.description,
|
||||
size: 40,
|
||||
color: Colors.blue.shade700),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
File(controller
|
||||
.beritaAcaraPath.value)
|
||||
.path
|
||||
.split('/')
|
||||
.last,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'Dokumen berita acara',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
controller.beritaAcaraPath.value = '';
|
||||
},
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
label: const Text('Hapus',
|
||||
style:
|
||||
TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
// Tidak ada berita acara
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _pickBeritaAcaraFile(context),
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: const Text('Upload Dokumen Berita Acara'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
side: BorderSide(color: Colors.blue.shade300),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Format dokumen: PDF, DOC, DOCX (maks. 10MB)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Tombol aksi
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton(
|
||||
onPressed: controller.isSaving.value
|
||||
? null
|
||||
: () => controller.updateLaporan(laporanId),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: controller.isSaving.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child:
|
||||
CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text(
|
||||
'Simpan Perubahan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk item informasi
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text(' : '),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk input teks
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
int maxLines = 1,
|
||||
bool required = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
children: required
|
||||
? const [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
maxLines: maxLines,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk memilih gambar dokumentasi
|
||||
Future<void> _pickDocumentationImage(BuildContext context) async {
|
||||
try {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
controller.dokumentasiPath.value = image.path;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Terjadi kesalahan: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk memilih file berita acara
|
||||
Future<void> _pickBeritaAcaraFile(BuildContext context) async {
|
||||
try {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'doc', 'docx'],
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
controller.beritaAcaraPath.value = result.files.single.path!;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Terjadi kesalahan: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,410 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:penyaluran_app/app/widgets/custom_app_bar.dart';
|
||||
import 'package:penyaluran_app/app/widgets/section_header.dart';
|
||||
import 'package:penyaluran_app/app/widgets/status_badge.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LaporanPenyaluranView extends GetView<LaporanPenyaluranController> {
|
||||
const LaporanPenyaluranView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: 'Laporan Penyaluran Bantuan',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Filter status
|
||||
_buildStatusFilter(),
|
||||
|
||||
// Daftar laporan
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.daftarLaporan.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.note_alt_outlined,
|
||||
size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Belum ada laporan penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Buat laporan baru untuk penyaluran yang telah selesai',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showPenyaluranDialog(context),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Buat Laporan Baru'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter berdasarkan status jika dipilih
|
||||
final filteredLaporan = controller.filterStatus.value == 'SEMUA'
|
||||
? controller.daftarLaporan
|
||||
: controller.daftarLaporan
|
||||
.where((laporan) =>
|
||||
laporan.status == controller.filterStatus.value)
|
||||
.toList();
|
||||
|
||||
if (filteredLaporan.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Tidak ada laporan dengan status ${controller.filterStatus.value}'),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchLaporan();
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: filteredLaporan.length,
|
||||
itemBuilder: (context, index) {
|
||||
final laporan = filteredLaporan[index];
|
||||
return _buildLaporanCard(context, laporan);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showPenyaluranDialog(context),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget untuk filter status
|
||||
Widget _buildStatusFilter() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
color: AppTheme.primaryColor.withOpacity(0.05),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
title: 'Filter Status',
|
||||
// subtitle: 'Tampilkan laporan berdasarkan status',
|
||||
// showDivider: false,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip('SEMUA'),
|
||||
_buildFilterChip('DRAFT'),
|
||||
_buildFilterChip('FINAL'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Chip untuk filter
|
||||
Widget _buildFilterChip(String status) {
|
||||
return Obx(() {
|
||||
final isSelected = controller.filterStatus.value == status;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(status),
|
||||
onSelected: (_) {
|
||||
controller.filterStatus.value = status;
|
||||
},
|
||||
backgroundColor: Colors.white,
|
||||
checkmarkColor: Colors.white,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Widget untuk card laporan
|
||||
Widget _buildLaporanCard(
|
||||
BuildContext context, LaporanPenyaluranModel laporan) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.id);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
laporan.judul,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
StatusBadge(status: laporan.status ?? 'DRAFT'),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24, color: Colors.white30),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Tanggal: ${laporan.tanggalLaporan != null ? DateTimeHelper.formatDateTime(laporan.tanggalLaporan!) : '-'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (laporan.status == 'FINAL')
|
||||
Material(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
controller
|
||||
.fetchPenyaluranDetail(
|
||||
laporan.penyaluranBantuanId)
|
||||
.then((_) {
|
||||
if (controller.selectedPenyaluran.value != null) {
|
||||
controller.exportToPdf(laporan,
|
||||
controller.selectedPenyaluran.value!);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.picture_as_pdf,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'Ekspor PDF',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
Get.toNamed('/laporan-penyaluran/edit',
|
||||
arguments: laporan.id);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.edit, color: Colors.orange, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('Edit',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
_showDeleteConfirmation(context, laporan.id!);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.delete, color: Colors.red, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('Hapus',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog konfirmasi hapus laporan
|
||||
void _showDeleteConfirmation(BuildContext context, String laporanId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Hapus Laporan'),
|
||||
content: const Text('Apakah Anda yakin ingin menghapus laporan ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
controller.deleteLaporan(laporanId);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog pilih penyaluran untuk laporan baru
|
||||
void _showPenyaluranDialog(BuildContext context) {
|
||||
if (controller.penyaluranTanpaLaporan.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Tidak ada penyaluran yang tersedia untuk dibuat laporan. Pastikan ada penyaluran dengan status SELESAI.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Pilih Penyaluran'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.penyaluranTanpaLaporan.length,
|
||||
itemBuilder: (context, index) {
|
||||
final penyaluran = controller.penyaluranTanpaLaporan[index];
|
||||
return ListTile(
|
||||
title: Text(
|
||||
penyaluran.nama ??
|
||||
'Penyaluran #${penyaluran.id?.substring(0, 8)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Tanggal: ${penyaluran.tanggalSelesai != null ? DateFormat('dd/MM/yyyy').format(penyaluran.tanggalSelesai!) : '-'}',
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
// Arahkan ke halaman buat laporan dengan ID penyaluran
|
||||
Get.toNamed('/laporan-penyaluran/create',
|
||||
arguments: penyaluran.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||
@ -6,7 +7,6 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/laporan_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
|
||||
|
||||
@ -60,8 +60,8 @@ class PetugasDesaBinding extends Bindings {
|
||||
);
|
||||
|
||||
// Daftarkan controller laporan
|
||||
Get.lazyPut<LaporanController>(
|
||||
() => LaporanController(),
|
||||
Get.lazyPut<LaporanPenyaluranController>(
|
||||
() => LaporanPenyaluranController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/skema_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_penyaluran_model.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/controllers/laporan_penyaluran_controller.dart';
|
||||
|
||||
class DetailPenyaluranController extends GetxController {
|
||||
final SupabaseService _supabaseService = Get.find<SupabaseService>();
|
||||
@ -14,10 +16,16 @@ class DetailPenyaluranController extends GetxController {
|
||||
final penyaluran = Rx<PenyaluranBantuanModel?>(null);
|
||||
final skemaBantuan = Rx<SkemaBantuanModel?>(null);
|
||||
final penerimaPenyaluran = <PenerimaPenyaluranModel>[].obs;
|
||||
final laporan = Rx<LaporanPenyaluranModel?>(null);
|
||||
final isLoadingLaporan = false.obs;
|
||||
|
||||
// Status untuk mengetahui apakah petugas desa
|
||||
final isPetugasDesa = false.obs;
|
||||
|
||||
// Tambahkan referensi ke controller laporan
|
||||
LaporanPenyaluranController? laporanController;
|
||||
final RxBool isExporting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -432,6 +440,9 @@ class DetailPenyaluranController extends GetxController {
|
||||
}
|
||||
penerimaPenyaluran.assignAll(penerima);
|
||||
|
||||
// Periksa apakah ada laporan untuk penyaluran ini
|
||||
await checkLaporanPenyaluran(penyaluranId);
|
||||
|
||||
// if (penerima.isNotEmpty) {
|
||||
// print('DetailPenyaluranController - ID penerima: ${penerima[0].id}');
|
||||
// }
|
||||
@ -446,6 +457,57 @@ class DetailPenyaluranController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkLaporanPenyaluran(String penyaluranId) async {
|
||||
try {
|
||||
isLoadingLaporan.value = true;
|
||||
|
||||
final response = await _supabaseService.client
|
||||
.from('laporan_penyaluran')
|
||||
.select('*')
|
||||
.eq('penyaluran_bantuan_id', penyaluranId)
|
||||
.maybeSingle();
|
||||
|
||||
if (response != null) {
|
||||
// Laporan ditemukan
|
||||
laporan.value = LaporanPenyaluranModel.fromJson(response);
|
||||
} else {
|
||||
// Tidak ada laporan
|
||||
laporan.value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error saat memeriksa laporan penyaluran: $e');
|
||||
laporan.value = null;
|
||||
} finally {
|
||||
isLoadingLaporan.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void navigateToLaporanCreate() {
|
||||
if (penyaluran.value?.id == null) return;
|
||||
|
||||
// Kirim ID penyaluran langsung sebagai argument (String), bukan dalam bentuk Map
|
||||
Get.toNamed('/laporan-penyaluran/create', arguments: penyaluran.value!.id)
|
||||
?.then((value) {
|
||||
if (value == true) {
|
||||
// Refresh data setelah membuat laporan
|
||||
checkLaporanPenyaluran(penyaluran.value!.id!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void navigateToLaporanDetail() {
|
||||
if (laporan.value?.id == null) return;
|
||||
|
||||
// Navigasi ke halaman detail dengan mengirimkan ID sebagai argument
|
||||
Get.toNamed('/laporan-penyaluran/detail', arguments: laporan.value!.id)
|
||||
?.then((value) {
|
||||
if (value == true) {
|
||||
// Refresh data setelah melihat detail laporan
|
||||
checkLaporanPenyaluran(penyaluran.value!.id!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Metode untuk verifikasi penerima berdasarkan QR code
|
||||
Future<bool> verifikasiPenerimaByQrCode(
|
||||
String penyaluranId, String qrHash) async {
|
||||
@ -498,4 +560,53 @@ class DetailPenyaluranController extends GetxController {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk memuat controller laporan
|
||||
Future<void> loadLaporanPenyaluranController() async {
|
||||
if (laporanController == null) {
|
||||
// Cek apakah controller sudah ada di Get
|
||||
if (Get.isRegistered<LaporanPenyaluranController>()) {
|
||||
laporanController = Get.find<LaporanPenyaluranController>();
|
||||
} else {
|
||||
// Jika belum ada, buat instance baru
|
||||
laporanController = Get.put(LaporanPenyaluranController());
|
||||
}
|
||||
}
|
||||
|
||||
// Pastikan data laporan dimuat
|
||||
if (laporan.value != null && penyaluran.value != null) {
|
||||
await laporanController!.fetchLaporanDetail(laporan.value!.id!);
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk export PDF
|
||||
Future<void> exportToPdf() async {
|
||||
if (laporan.value == null || penyaluran.value == null) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Data laporan atau penyaluran tidak tersedia',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isExporting.value = true;
|
||||
try {
|
||||
await loadLaporanPenyaluranController();
|
||||
await laporanController!.exportToPdf(laporan.value!, penyaluran.value!);
|
||||
} catch (e) {
|
||||
print('Error saat export PDF: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengekspor laporan ke PDF',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
} finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,197 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/data/models/laporan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
|
||||
class LaporanController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
final SupabaseService _supabaseService = SupabaseService.to;
|
||||
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Indeks kategori yang dipilih untuk filter
|
||||
final RxInt selectedCategoryIndex = 0.obs;
|
||||
|
||||
// Data untuk laporan
|
||||
final RxList<LaporanModel> daftarLaporan = <LaporanModel>[].obs;
|
||||
|
||||
// Filter tanggal
|
||||
final Rx<DateTime?> tanggalMulai = Rx<DateTime?>(null);
|
||||
final Rx<DateTime?> tanggalSelesai = Rx<DateTime?>(null);
|
||||
|
||||
// Controller untuk pencarian
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
UserModel? get user => _authController.user;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Set default tanggal filter (1 bulan terakhir)
|
||||
tanggalSelesai.value = DateTime.now();
|
||||
tanggalMulai.value = DateTime.now().subtract(const Duration(days: 30));
|
||||
loadLaporanData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> loadLaporanData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final laporanData = await _supabaseService.getLaporan(
|
||||
tanggalMulai.value,
|
||||
tanggalSelesai.value,
|
||||
);
|
||||
if (laporanData != null) {
|
||||
daftarLaporan.value =
|
||||
laporanData.map((data) => LaporanModel.fromJson(data)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading laporan data: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> generateLaporan(String jenis) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final laporan = LaporanModel(
|
||||
jenis: jenis,
|
||||
tanggalMulai: tanggalMulai.value,
|
||||
tanggalSelesai: tanggalSelesai.value,
|
||||
petugasId: user?.id,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final laporanId =
|
||||
await _supabaseService.generateLaporan(laporan.toJson());
|
||||
|
||||
if (laporanId != null) {
|
||||
await loadLaporanData();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil dibuat',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error generating laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal membuat laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> downloadLaporan(String laporanId) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final url = await _supabaseService.downloadLaporan(laporanId);
|
||||
if (url != null) {
|
||||
// Implementasi download file
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil diunduh',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error downloading laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal mengunduh laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteLaporan(String laporanId) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await _supabaseService.deleteLaporan(laporanId);
|
||||
await loadLaporanData();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Laporan berhasil dihapus',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error deleting laporan: $e');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus laporan: ${e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void setTanggalMulai(DateTime tanggal) {
|
||||
tanggalMulai.value = tanggal;
|
||||
}
|
||||
|
||||
void setTanggalSelesai(DateTime tanggal) {
|
||||
tanggalSelesai.value = tanggal;
|
||||
}
|
||||
|
||||
Future<void> applyFilter() async {
|
||||
await loadLaporanData();
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadLaporanData();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void changeCategory(int index) {
|
||||
selectedCategoryIndex.value = index;
|
||||
}
|
||||
|
||||
List<LaporanModel> getFilteredLaporan() {
|
||||
switch (selectedCategoryIndex.value) {
|
||||
case 0:
|
||||
return daftarLaporan;
|
||||
case 1:
|
||||
return daftarLaporan
|
||||
.where((item) => item.jenis == 'PENYALURAN')
|
||||
.toList();
|
||||
case 2:
|
||||
return daftarLaporan
|
||||
.where((item) => item.jenis == 'STOK_BANTUAN')
|
||||
.toList();
|
||||
case 3:
|
||||
return daftarLaporan.where((item) => item.jenis == 'PENERIMA').toList();
|
||||
default:
|
||||
return daftarLaporan;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerima_page.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/qr_scanner_page.dart';
|
||||
|
||||
class DetailPenyaluranPage extends StatelessWidget {
|
||||
@ -69,6 +68,9 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
controller.penyaluran.value?.alasanPembatalan != null &&
|
||||
controller.penyaluran.value!.alasanPembatalan!.isNotEmpty)
|
||||
_buildPembatalanSection(context),
|
||||
if (controller.penyaluran.value?.status?.toUpperCase() ==
|
||||
'TERLAKSANA')
|
||||
_buildLaporanSection(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildPenerimaPenyaluranSection(context),
|
||||
const SizedBox(height: 24),
|
||||
@ -1495,6 +1497,220 @@ class DetailPenyaluranPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLaporanSection(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingLaporan.value) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 8),
|
||||
Text('Memuat data laporan...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppTheme.successColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Laporan Penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (controller.laporan.value != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppTheme.successColor.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: AppTheme.successColor,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
if (controller.laporan.value == null)
|
||||
Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_late_outlined,
|
||||
size: 50,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Belum ada laporan penyaluran',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Buat laporan untuk mendokumentasikan hasil penyaluran bantuan',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton.icon(
|
||||
onPressed: controller.navigateToLaporanCreate,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
label: const Text('Buat Laporan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Judul', controller.laporan.value!.judul),
|
||||
_buildInfoRow(
|
||||
'Tanggal Laporan',
|
||||
controller.laporan.value?.tanggalLaporan != null
|
||||
? DateTimeHelper.formatDateTime(
|
||||
controller.laporan.value!.tanggalLaporan!)
|
||||
: '-',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
controller.navigateToLaporanDetail(),
|
||||
icon: const Icon(Icons.visibility),
|
||||
label: const Text('Lihat Detail'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.primaryColor,
|
||||
side: const BorderSide(
|
||||
color: AppTheme.primaryColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (controller.laporan.value?.beritaAcaraUrl != null &&
|
||||
controller
|
||||
.laporan.value!.beritaAcaraUrl!.isNotEmpty)
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton.icon(
|
||||
onPressed: controller.isExporting.value
|
||||
? null
|
||||
: () => controller.exportToPdf(),
|
||||
icon: controller.isExporting.value
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
label: Text(controller.isExporting.value
|
||||
? 'Mengekspor...'
|
||||
: 'Unduh PDF'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12),
|
||||
disabledBackgroundColor:
|
||||
AppTheme.successColor.withOpacity(0.7),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<PenerimaPenyaluranModel> _getFilteredPenerima() {
|
||||
final query = searchQuery.value;
|
||||
final status = statusFilter.value;
|
||||
|
@ -179,18 +179,25 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
backgroundImage: controller.user?.avatar != null &&
|
||||
controller.user!.avatar!.isNotEmpty
|
||||
? NetworkImage(controller.user!.avatar!)
|
||||
: null,
|
||||
child: controller.user?.avatar == null ||
|
||||
controller.user!.avatar!.isEmpty
|
||||
? const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: AppTheme.primaryColor,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
controller.nama,
|
||||
controller.user?.name ?? 'Petugas Desa',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@ -198,203 +205,106 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Petugas Desa',
|
||||
controller.user?.desa?.nama != null
|
||||
? '${controller.user?.role} - ${controller.user!.desa!.nama}'
|
||||
: controller.user?.role ?? 'PETUGAS_DESA',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: Colors.white.withAlpha(200),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
selected: controller.activeTabIndex.value == 0,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(0);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.calendar_today_outlined),
|
||||
title: const Text('Penyaluran'),
|
||||
selected: controller.activeTabIndex.value == 1,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() => ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.handshake_outlined),
|
||||
if (controller.jumlahMenunggu.value > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahMenunggu.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Penitipan'),
|
||||
selected: controller.activeTabIndex.value == 2,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(2);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
Obx(() {
|
||||
final int jumlahPengaduanDiproses = controller.jumlahDiproses.value;
|
||||
|
||||
return ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.report_problem_outlined),
|
||||
// Selalu tampilkan badge untuk debugging
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
jumlahPengaduanDiproses.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Pengaduan'),
|
||||
selected: controller.activeTabIndex.value == 3,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(3);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}),
|
||||
Obx(() => ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Stok Bantuan'),
|
||||
selected: controller.activeTabIndex.value == 4,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
controller.changeTab(4);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people_outline),
|
||||
title: const Text('Daftar Penerima'),
|
||||
leading: const Icon(Icons.dashboard_outlined),
|
||||
title: const Text('Dashboard'),
|
||||
selected: controller.activeTabIndex.value == 0,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(0);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.handshake_outlined),
|
||||
title: const Text('Penyaluran'),
|
||||
selected: controller.activeTabIndex.value == 1,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(1);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_2_outlined),
|
||||
title: const Text('Penitipan'),
|
||||
selected: controller.activeTabIndex.value == 2,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(2);
|
||||
},
|
||||
),
|
||||
Obx(() => ListTile(
|
||||
leading: controller.jumlahDiproses.value > 0
|
||||
? Badge(
|
||||
label: Text(controller.jumlahDiproses.value.toString()),
|
||||
backgroundColor: Colors.red,
|
||||
child: const Icon(Icons.support_outlined),
|
||||
)
|
||||
: const Icon(Icons.support_outlined),
|
||||
title: const Text('Pengaduan'),
|
||||
selected: controller.activeTabIndex.value == 3,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(3);
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory_outlined),
|
||||
title: const Text('Stok Bantuan'),
|
||||
selected: controller.activeTabIndex.value == 4,
|
||||
selectedColor: AppTheme.primaryColor,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
controller.changeTab(4);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: const Text('Kelola Penerima'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-penerima');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.volunteer_activism_outlined),
|
||||
title: const Text('Daftar Donatur'),
|
||||
leading: const Icon(Icons.people_outlined),
|
||||
title: const Text('Kelola Donatur'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/daftar-donatur');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.notifications_outlined),
|
||||
if (controller.jumlahNotifikasiBelumDibaca.value > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 12,
|
||||
minHeight: 12,
|
||||
),
|
||||
child: Text(
|
||||
controller.jumlahNotifikasiBelumDibaca.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text('Notifikasi'),
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
title: const Text('Laporan Penyaluran'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // Tutup drawer terlebih dahulu
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NotifikasiView(),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/laporan-penyaluran');
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profil'),
|
||||
onTap: () {
|
||||
// Navigasi ke halaman profil
|
||||
Navigator.pop(context);
|
||||
Get.toNamed('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: const Text('Pengaturan'),
|
||||
onTap: () {
|
||||
// Navigasi ke halaman pengaturan
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Keluar'),
|
||||
|
@ -98,7 +98,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'terlaksana' : 'batal terlaksana'}',
|
||||
'Daftar Penyaluran ${status == 'TERLAKSANA' ? 'Terlaksana' : 'Batal'}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -108,6 +108,11 @@ class WargaView extends GetView<WargaDashboardController> {
|
||||
badgeColor: Colors.orange,
|
||||
onTap: () => controller.changeTab(2),
|
||||
),
|
||||
DrawerMenuItem(
|
||||
icon: Icons.description_outlined,
|
||||
title: 'Laporan Penyaluran',
|
||||
onTap: () => Get.toNamed('/laporan-penyaluran'),
|
||||
),
|
||||
],
|
||||
)),
|
||||
body: Obx(() {
|
||||
|
@ -31,6 +31,11 @@ import 'package:penyaluran_app/app/modules/warga/views/warga_detail_penerimaan_v
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/detail_pengaduan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/pengaduan_binding.dart';
|
||||
import 'package:penyaluran_app/app/modules/warga/views/detail_pengaduan_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_detail_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_create_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/views/laporan_penyaluran_edit_view.dart';
|
||||
import 'package:penyaluran_app/app/modules/laporan_penyaluran/bindings/laporan_penyaluran_binding.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
@ -148,6 +153,26 @@ class AppPages {
|
||||
page: () => const RiwayatPengaduanView(),
|
||||
binding: RiwayatPengaduanBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran,
|
||||
page: () => const LaporanPenyaluranView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/detail',
|
||||
page: () => const LaporanPenyaluranDetailView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/create',
|
||||
page: () => const LaporanPenyaluranCreateView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.laporanPenyaluran + '/edit',
|
||||
page: () => const LaporanPenyaluranEditView(),
|
||||
binding: LaporanPenyaluranBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.qrScanner,
|
||||
page: () => QrScannerPage(penyaluranId: Get.parameters['id'] ?? ''),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_colors.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
/// AppBar kustom yang digunakan di seluruh aplikasi
|
||||
///
|
||||
@ -60,7 +60,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
centerTitle: centerTitle,
|
||||
elevation: elevation,
|
||||
backgroundColor: backgroundColor ?? AppColors.primary,
|
||||
backgroundColor: backgroundColor ?? AppTheme.primaryColor,
|
||||
foregroundColor: foregroundColor ?? Colors.white,
|
||||
leading: _buildLeading(),
|
||||
actions: actions,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
@ -20,20 +21,22 @@ class StatusBadge extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final String statusUpper = status.toUpperCase();
|
||||
|
||||
// Default colors for common statuses
|
||||
// Default colors for common statuses menggunakan AppTheme
|
||||
final Map<String, Color> defaultColors = {
|
||||
'DITERIMA': Colors.green,
|
||||
'MENUNGGU': Colors.orange,
|
||||
'DITOLAK': Colors.red,
|
||||
'PROSES': Colors.orange,
|
||||
'DIPROSES': Colors.orange,
|
||||
'TINDAKAN': Colors.orange,
|
||||
'SELESAI': Colors.green,
|
||||
'TERVERIFIKASI': Colors.green,
|
||||
'BELUMMENERIMA': Colors.orange,
|
||||
'DIJADWALKAN': Colors.blue,
|
||||
'TERLAKSANA': Colors.purple,
|
||||
'AKTIF': Colors.green,
|
||||
'DITERIMA': AppTheme.verifiedColor,
|
||||
'MENUNGGU': AppTheme.processedColor,
|
||||
'DITOLAK': AppTheme.rejectedColor,
|
||||
'PROSES': AppTheme.processedColor,
|
||||
'DIPROSES': AppTheme.processedColor,
|
||||
'TINDAKAN': AppTheme.processedColor,
|
||||
'SELESAI': AppTheme.completedColor,
|
||||
'TERVERIFIKASI': AppTheme.verifiedColor,
|
||||
'BELUMMENERIMA': AppTheme.processedColor,
|
||||
'DIJADWALKAN': AppTheme.scheduledColor,
|
||||
'TERLAKSANA': AppTheme.completedColor,
|
||||
'AKTIF': AppTheme.verifiedColor,
|
||||
'DRAFT': AppTheme.processedColor,
|
||||
'FINAL': AppTheme.completedColor,
|
||||
};
|
||||
|
||||
// Default labels for common statuses
|
||||
@ -50,6 +53,8 @@ class StatusBadge extends StatelessWidget {
|
||||
'DIJADWALKAN': 'Dijadwalkan',
|
||||
'TERLAKSANA': 'Terlaksana',
|
||||
'AKTIF': 'Aktif',
|
||||
'DRAFT': 'Draft',
|
||||
'FINAL': 'Final',
|
||||
};
|
||||
|
||||
// Determine color and label based on status
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <open_file_linux/open_file_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
||||
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
open_file_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -6,16 +6,20 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import open_file_mac
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
116
pubspec.lock
116
pubspec.lock
@ -57,6 +57,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.12"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -153,6 +169,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.1"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -350,7 +374,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
@ -533,6 +557,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file
|
||||
sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.10"
|
||||
open_file_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_android
|
||||
sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
open_file_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_ios
|
||||
sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
open_file_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_linux
|
||||
sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
open_file_mac:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_mac
|
||||
sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
open_file_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_platform_interface
|
||||
sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
open_file_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_web
|
||||
sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
open_file_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: open_file_windows
|
||||
sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -550,7 +638,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@ -597,6 +685,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -814,34 +910,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_flutter_calendar
|
||||
sha256: "11b01bc7ad1d240d7c644081bda79e61c0a8d26eec7eba67bfc7274310562897"
|
||||
sha256: "87761c6e73997d8d39181741f9e6244daf29b521aafaccf46b1bf31e86767a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "28.2.11"
|
||||
version: "28.2.12"
|
||||
syncfusion_flutter_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "59b6d2a7deacade6129d2f15615ca49ed56278fea055cd2e52cace78a343dd5e"
|
||||
sha256: f1d2b52697543e13bdefdc62d15868124a265987577f53224a7dbe176c8448f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "28.2.11"
|
||||
version: "28.2.12"
|
||||
syncfusion_flutter_datepicker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_datepicker
|
||||
sha256: "73ece73742f123c750d674461c6902cbdf32fbd695c15fdf7e8487d290bb7179"
|
||||
sha256: cfc91ebacee63b2c5220e541736f8df211d4f0bfbf34265778862ae20faf094f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "28.2.11+1"
|
||||
version: "28.2.12"
|
||||
syncfusion_localizations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_localizations
|
||||
sha256: "04bddcd326628ae3aea227d86534b1682e893674df3474fc71c83e0bffb27325"
|
||||
sha256: "383f0bbd80cdd1bf37c72a4ff3fb0cb476f34e753dab4dd146ae57578fb1fa50"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "28.2.11"
|
||||
version: "28.2.12"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
14
pubspec.yaml
14
pubspec.yaml
@ -30,6 +30,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: ^1.3.0 # atau versi yang lebih baru
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
@ -63,6 +64,8 @@ dependencies:
|
||||
|
||||
# Image picker untuk mengambil gambar dari kamera atau galeri
|
||||
image_picker: ^1.1.2
|
||||
# File picker untuk memilih file dokumen
|
||||
file_picker: ^9.2.1
|
||||
syncfusion_flutter_calendar: ^28.2.11
|
||||
syncfusion_localizations: ^28.2.11
|
||||
signature: ^6.0.0
|
||||
@ -81,6 +84,13 @@ dependencies:
|
||||
# Untuk fungsi hash
|
||||
crypto: ^3.0.3
|
||||
|
||||
# Package untuk generate PDF
|
||||
pdf: ^3.10.8
|
||||
# Package untuk menyimpan file
|
||||
path_provider: ^2.1.2
|
||||
# Package untuk membuka file
|
||||
open_file: ^3.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@ -124,6 +134,10 @@ flutter:
|
||||
- asset: assets/font/DMSans-VariableFont_opsz,wght.ttf
|
||||
- asset: assets/font/DMSans-Italic-VariableFont_opsz,wght.ttf
|
||||
style: italic
|
||||
- asset: assets/font/DMSans-Regular.ttf
|
||||
weight: 400
|
||||
- asset: assets/font/DMSans-Bold.ttf
|
||||
weight: 700
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
|
Reference in New Issue
Block a user