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:
Khafidh Fuadi
2025-03-20 05:19:04 +07:00
parent 3b12c7af86
commit 54c4660302
29 changed files with 4702 additions and 539 deletions

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <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|

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><EFBFBD><EFBFBD><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~

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <08><><EFBFBD><EFBFBD><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

View File

@ -2,27 +2,27 @@ C/C++ Structured LogO
M
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
A
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint  <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

Binary file not shown.

Binary file not shown.

View File

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

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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),

View File

@ -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,

View File

@ -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(() {

View File

@ -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'] ?? ''),

View File

@ -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,

View File

@ -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

View File

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

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
gtk
open_file_linux
url_launcher_linux
)

View File

@ -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"))

View File

@ -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:

View File

@ -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