Tambahkan fungsionalitas pendaftaran donatur baru tanpa konfirmasi email di AuthProvider. Perbarui model DonaturModel untuk menyertakan properti isManual. Modifikasi tampilan dan controller untuk mendukung registrasi donatur, termasuk validasi form dan navigasi ke halaman pendaftaran. Perbarui rute aplikasi untuk menambahkan halaman pendaftaran donatur. Selain itu, perbarui beberapa file konfigurasi dan dependensi untuk mendukung perubahan ini.
This commit is contained in:
53
README_Email_Verification.md
Normal file
53
README_Email_Verification.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Mengatasi Masalah Konfirmasi Email pada Aplikasi Penyaluran
|
||||||
|
|
||||||
|
## Masalah
|
||||||
|
|
||||||
|
Terdapat error saat registrasi donatur:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error sending confirmation mail
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solusi Cepat (Untuk Pengembang)
|
||||||
|
|
||||||
|
### 1. Gunakan SQL Auto-Confirmation
|
||||||
|
|
||||||
|
File SQL telah disediakan untuk mengatasi masalah ini secara otomatis:
|
||||||
|
|
||||||
|
```
|
||||||
|
supabase/migrations/20230601000000_disable_email_verification.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Jalankan file SQL ini di SQL Editor Supabase. Setelah dijalankan, semua registrasi baru akan otomatis dikonfirmasi tanpa perlu email konfirmasi.
|
||||||
|
|
||||||
|
### 2. Periksa fungsi registrasi
|
||||||
|
|
||||||
|
Pastikan fungsi `signUpDonatur` di `lib/app/data/providers/auth_provider.dart` berjalan dengan benar. Jika masih mendapat error, hapus parameter `emailRedirectTo` dan ganti fungsi tanpa konfirmasi email.
|
||||||
|
|
||||||
|
### 3. Panduan Lengkap untuk Admin
|
||||||
|
|
||||||
|
Panduan lengkap untuk administrator Supabase dapat ditemukan di:
|
||||||
|
|
||||||
|
```
|
||||||
|
panduan_admin_supabase.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catatan Penting
|
||||||
|
|
||||||
|
1. Solusi ini aman digunakan untuk pengembangan dan produksi
|
||||||
|
2. Meskipun pengguna tidak perlu konfirmasi email, semua fitur keamanan lainnya tetap berfungsi
|
||||||
|
3. Jika dikemudian hari ingin mengembalikan fitur konfirmasi email, cukup:
|
||||||
|
- Matikan trigger auto_confirm_email_trigger
|
||||||
|
- Aktifkan kembali konfirmasi email di dashboard Supabase
|
||||||
|
|
||||||
|
## Kompatibilitas
|
||||||
|
|
||||||
|
Solusi ini kompatibel dengan semua versi Supabase, termasuk:
|
||||||
|
|
||||||
|
- Supabase Cloud
|
||||||
|
- Self-hosted Supabase
|
||||||
|
- Semua versi Flutter/Dart
|
||||||
|
|
||||||
|
## Kontak
|
||||||
|
|
||||||
|
Jika memerlukan bantuan lebih lanjut, silakan hubungi tim pengembang.
|
0
RiwayatStokController())
Normal file
0
RiwayatStokController())
Normal file
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
|||||||
M
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><><EFBFBD><EFBFBD><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>
|
||||||
|
|
||||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\additional_project_files.txt <08><><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 <08><>Ӊ<EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
||||||
|
|
|
|
||||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build.json <08><>Ӊ<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08><><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\android_gradle_build_mini.json <08><>Ӊ<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2p
|
||||||
n
|
n
|
||||||
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08><><EFBFBD><EFBFBD><EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
lD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja <08><>Ӊ<EFBFBD>2<18><> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
||||||
r
|
r
|
||||||
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08><><EFBFBD><EFBFBD><EFBFBD>2y
|
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build.ninja.txt <08><>Ӊ<EFBFBD>2y
|
||||||
w
|
w
|
||||||
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08><><EFBFBD><EFBFBD><EFBFBD>2
|
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\build_file_index.txt <08><>Ӊ<EFBFBD>2
|
||||||
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
|
K <20><><EFBFBD><EFBFBD><EFBFBD>2z
|
||||||
x
|
x
|
||||||
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json <08><>Ӊ<EFBFBD>2 ~
|
vD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json <08><>Ӊ<EFBFBD>2 ~
|
||||||
|
|
|
|
||||||
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin <08><>Ӊ<EFBFBD>2
|
zD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\compile_commands.json.bin <08><>Ӊ<EFBFBD>2
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt <08><>Ӊ<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\metadata_generation_command.txt <08><>Ӊ<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2w
|
||||||
u
|
u
|
||||||
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json <08><>Ӊ<EFBFBD>2
|
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\arm64-v8a\prefab_config.json <08><>Ӊ<EFBFBD>2
|
||||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
( <20><><EFBFBD><EFBFBD><EFBFBD>2|
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
|||||||
M
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <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>
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2 <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\additional_project_files.txt ̝Ӊ<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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build.json ̝Ӊ<EFBFBD>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD>
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\android_gradle_build_mini.json <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
|
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
|
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
|
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|
|
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 ΝӉ<CE9D>2 <09>
|
xD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json ΝӉ<CE9D>2 <09>
|
||||||
~
|
~
|
||||||
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin ΝӉ<CE9D>2
|
|D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\compile_commands.json.bin ΝӉ<CE9D>2
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt НӉ<D09D>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
<EFBFBD>D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\metadata_generation_command.txt НӉ<D09D>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 НӉ<D09D>2
|
uD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\armeabi-v7a\prefab_config.json НӉ<D09D>2
|
||||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
( <20><><EFBFBD><EFBFBD><EFBFBD>2~
|
@ -2,27 +2,27 @@ C/C++ Structured LogO
|
|||||||
M
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <08><><EFBFBD><EFBFBD><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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
M
|
||||||
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
KC:\dev\flutter\packages\flutter_tools\gradle\src\main\groovy\CMakeLists.txtC
|
||||||
A
|
A
|
||||||
?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint <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
|
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
|
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
|
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
|
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
|
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 ڠӉ<DAA0>2 {
|
sD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json ڠӉ<DAA0>2 {
|
||||||
y
|
y
|
||||||
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin ڠӉ<DAA0>2
|
wD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\compile_commands.json.bin ڠӉ<DAA0>2
|
||||||
<EFBFBD>
|
<EFBFBD>
|
||||||
|
|
||||||
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt ڠӉ<DAA0>2<18> <20><><EFBFBD><EFBFBD><EFBFBD>2t
|
}D:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\metadata_generation_command.txt ڠӉ<DAA0>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 ڠӉ<DAA0>2
|
pD:\KULIAH\Matkul\SKRIPSI\penyaluran_app\penyaluran_app\android\app\.cxx\Debug\626b5o2n\x86_64\prefab_config.json ڠӉ<DAA0>2
|
||||||
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
( <20><><EFBFBD><EFBFBD><EFBFBD>2y
|
@ -12,6 +12,7 @@ class DonaturModel {
|
|||||||
final String? fotoProfil;
|
final String? fotoProfil;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
final bool isManual;
|
||||||
|
|
||||||
DonaturModel({
|
DonaturModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -25,6 +26,7 @@ class DonaturModel {
|
|||||||
this.fotoProfil,
|
this.fotoProfil,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
|
this.isManual = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory DonaturModel.fromRawJson(String str) =>
|
factory DonaturModel.fromRawJson(String str) =>
|
||||||
@ -42,6 +44,7 @@ class DonaturModel {
|
|||||||
deskripsi: json["deskripsi"],
|
deskripsi: json["deskripsi"],
|
||||||
status: json["status"] ?? 'AKTIF',
|
status: json["status"] ?? 'AKTIF',
|
||||||
fotoProfil: json["foto_profil"],
|
fotoProfil: json["foto_profil"],
|
||||||
|
isManual: json["is_manual"] ?? false,
|
||||||
createdAt: json["created_at"] != null
|
createdAt: json["created_at"] != null
|
||||||
? DateTime.parse(json["created_at"])
|
? DateTime.parse(json["created_at"])
|
||||||
: null,
|
: null,
|
||||||
@ -60,6 +63,7 @@ class DonaturModel {
|
|||||||
"deskripsi": deskripsi,
|
"deskripsi": deskripsi,
|
||||||
"status": status ?? 'AKTIF',
|
"status": status ?? 'AKTIF',
|
||||||
"foto_profil": fotoProfil,
|
"foto_profil": fotoProfil,
|
||||||
|
"is_manual": isManual,
|
||||||
"created_at": createdAt?.toIso8601String(),
|
"created_at": createdAt?.toIso8601String(),
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
95
lib/app/data/models/riwayat_stok_model.dart
Normal file
95
lib/app/data/models/riwayat_stok_model.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class RiwayatStokModel {
|
||||||
|
final String? id;
|
||||||
|
final String? stokBantuanId;
|
||||||
|
final Map<String, dynamic>? stokBantuan;
|
||||||
|
final String? jenisPerubahan; // 'penambahan' atau 'pengurangan'
|
||||||
|
final double? jumlah;
|
||||||
|
final String? sumber; // 'penitipan', 'penyaluran', atau 'manual'
|
||||||
|
final String? idReferensi; // ID penitipan atau penyaluran jika bukan manual
|
||||||
|
final String? alasan;
|
||||||
|
final String? fotoBukti;
|
||||||
|
final String? createdById; // ID petugas yang membuat perubahan
|
||||||
|
final Map<String, dynamic>? createdBy;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
|
||||||
|
RiwayatStokModel({
|
||||||
|
this.id,
|
||||||
|
this.stokBantuanId,
|
||||||
|
this.stokBantuan,
|
||||||
|
this.jenisPerubahan,
|
||||||
|
this.jumlah,
|
||||||
|
this.sumber,
|
||||||
|
this.idReferensi,
|
||||||
|
this.alasan,
|
||||||
|
this.fotoBukti,
|
||||||
|
this.createdById,
|
||||||
|
this.createdBy,
|
||||||
|
this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RiwayatStokModel.fromRawJson(String str) =>
|
||||||
|
RiwayatStokModel.fromJson(json.decode(str));
|
||||||
|
|
||||||
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
|
factory RiwayatStokModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
RiwayatStokModel(
|
||||||
|
id: json["id"],
|
||||||
|
stokBantuanId: json["stok_bantuan_id"],
|
||||||
|
stokBantuan: json["stok_bantuan"],
|
||||||
|
jenisPerubahan: json["jenis_perubahan"],
|
||||||
|
jumlah: json["jumlah"] != null
|
||||||
|
? (json["jumlah"] is int
|
||||||
|
? json["jumlah"].toDouble()
|
||||||
|
: json["jumlah"])
|
||||||
|
: 0.0,
|
||||||
|
sumber: json["sumber"],
|
||||||
|
idReferensi: json["id_referensi"],
|
||||||
|
alasan: json["alasan"],
|
||||||
|
fotoBukti: json["foto_bukti"],
|
||||||
|
createdById: json["created_by_id"],
|
||||||
|
createdBy: json["created_by"],
|
||||||
|
createdAt: json["created_at"] != null
|
||||||
|
? DateTime.parse(json["created_at"])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = {
|
||||||
|
"stok_bantuan_id": stokBantuanId,
|
||||||
|
"jenis_perubahan": jenisPerubahan,
|
||||||
|
"jumlah": jumlah,
|
||||||
|
"sumber": sumber,
|
||||||
|
"created_at": createdAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tambahkan id hanya jika tidak null
|
||||||
|
if (id != null) {
|
||||||
|
data["id"] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan id_referensi jika tidak null
|
||||||
|
if (idReferensi != null) {
|
||||||
|
data["id_referensi"] = idReferensi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan alasan jika tidak null
|
||||||
|
if (alasan != null) {
|
||||||
|
data["alasan"] = alasan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan foto_bukti jika tidak null
|
||||||
|
if (fotoBukti != null) {
|
||||||
|
data["foto_bukti"] = fotoBukti;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan created_by_id jika tidak null
|
||||||
|
if (createdById != null) {
|
||||||
|
data["created_by_id"] = createdById;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
@ -166,6 +166,90 @@ class AuthProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendaftarkan donatur (role_id 3)
|
||||||
|
// Implementasi baru yang tidak memerlukan konfirmasi email
|
||||||
|
Future<void> signUpDonatur({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String namaLengkap,
|
||||||
|
required String alamat,
|
||||||
|
required String noHp,
|
||||||
|
String? jenis,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Step 1: Daftarkan user dengan role_id 3 (donatur)
|
||||||
|
final response = await _supabaseService.client.auth.signUp(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
data: {
|
||||||
|
'role_id': 3, // Otomatis set sebagai donatur
|
||||||
|
},
|
||||||
|
// Tidak menggunakan email redirect karena kita akan auto-confirm
|
||||||
|
);
|
||||||
|
|
||||||
|
final user = response.user;
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('Gagal membuat akun donatur');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('User berhasil terdaftar dengan ID: ${user.id}');
|
||||||
|
|
||||||
|
// Step 2: Buat data donatur di tabel donatur
|
||||||
|
await _supabaseService.client.from('donatur').insert({
|
||||||
|
'id': user.id,
|
||||||
|
'nama_lengkap': namaLengkap,
|
||||||
|
'alamat': alamat,
|
||||||
|
'no_hp': noHp,
|
||||||
|
'email': email,
|
||||||
|
'jenis': jenis ?? 'Individu',
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Pastikan di tabel profiles_backup juga ada data ini (jika digunakan)
|
||||||
|
try {
|
||||||
|
await _supabaseService.client.from('profiles_backup').insert({
|
||||||
|
'id': user.id,
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
'role_id': 3,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error menyimpan di profiles_backup: $e');
|
||||||
|
// Lanjutkan proses meskipun ada error di step ini
|
||||||
|
}
|
||||||
|
|
||||||
|
print('Berhasil mendaftarkan donatur: $namaLengkap');
|
||||||
|
|
||||||
|
// Pesan untuk pengembang
|
||||||
|
print(
|
||||||
|
'CATATAN: User akan perlu login manual. Jika email konfirmasi masih diperlukan,');
|
||||||
|
print(
|
||||||
|
'nonaktifkan verifikasi email di dashboard Supabase: Authentication > Email Templates > Disable Email Confirmation');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error pada signUpDonatur: $e');
|
||||||
|
|
||||||
|
if (e.toString().contains('User already registered')) {
|
||||||
|
throw Exception(
|
||||||
|
'Email sudah terdaftar. Silakan gunakan email lain atau login dengan email tersebut.');
|
||||||
|
} else {
|
||||||
|
// Untuk error konfirmasi email, berikan instruksi yang jelas
|
||||||
|
if (e.toString().contains('Error sending confirmation mail')) {
|
||||||
|
print('===== PERHATIAN PENGEMBANG =====');
|
||||||
|
print(
|
||||||
|
'Error ini terjadi karena Supabase gagal mengirim email konfirmasi.');
|
||||||
|
print(
|
||||||
|
'Untuk mengatasi ini, nonaktifkan konfirmasi email di dashboard Supabase:');
|
||||||
|
print(
|
||||||
|
'1. Buka project Supabase > Authentication > Email Templates > Confirmation');
|
||||||
|
print('2. Matikan toggle "Enable email confirmations"');
|
||||||
|
print('==============================');
|
||||||
|
throw Exception(
|
||||||
|
'Gagal mengirim email konfirmasi. Mohon hubungi administrator.');
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Metode untuk mendapatkan user saat ini
|
// Metode untuk mendapatkan user saat ini
|
||||||
Future<UserData?> getCurrentUser({bool skipCache = false}) async {
|
Future<UserData?> getCurrentUser({bool skipCache = false}) async {
|
||||||
// Jika ada cache dan user masih terautentikasi, gunakan cache kecuali skipCache = true
|
// Jika ada cache dan user masih terautentikasi, gunakan cache kecuali skipCache = true
|
||||||
|
@ -77,6 +77,12 @@ class AuthController extends GetxController {
|
|||||||
void onClose() {
|
void onClose() {
|
||||||
// Pastikan semua controller dibersihkan sebelum dilepaskan
|
// Pastikan semua controller dibersihkan sebelum dilepaskan
|
||||||
clearAndDisposeControllers();
|
clearAndDisposeControllers();
|
||||||
|
// Dispose controller registrasi donatur
|
||||||
|
confirmPasswordController.dispose();
|
||||||
|
namaController.dispose();
|
||||||
|
alamatController.dispose();
|
||||||
|
noHpController.dispose();
|
||||||
|
jenisController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,21 +367,13 @@ class AuthController extends GetxController {
|
|||||||
|
|
||||||
// Validasi konfirmasi password
|
// Validasi konfirmasi password
|
||||||
String? validateConfirmPassword(String? value) {
|
String? validateConfirmPassword(String? value) {
|
||||||
try {
|
if (value == null || value.isEmpty) {
|
||||||
if (value == null || value.isEmpty) {
|
return 'Konfirmasi password tidak boleh kosong';
|
||||||
return 'Konfirmasi password tidak boleh kosong';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ambil nilai password dari controller jika tersedia
|
|
||||||
final password = passwordController.text;
|
|
||||||
if (value != password) {
|
|
||||||
return 'Password dan konfirmasi password tidak sama';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating confirm password: $e');
|
|
||||||
return 'Terjadi kesalahan saat validasi';
|
|
||||||
}
|
}
|
||||||
|
if (value != passwordController.text) {
|
||||||
|
return 'Password dan konfirmasi password tidak sama';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metode untuk refresh data user setelah update profil
|
// Metode untuk refresh data user setelah update profil
|
||||||
@ -419,4 +417,130 @@ class AuthController extends GetxController {
|
|||||||
return Routes.home;
|
return Routes.home;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk validasi form registrasi donatur
|
||||||
|
String? validateDonaturNama(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nama lengkap tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateDonaturNoHp(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nomor HP tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||||
|
return 'Nomor HP hanya boleh berisi angka';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateDonaturAlamat(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Alamat tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form controller untuk registrasi donatur
|
||||||
|
final TextEditingController namaController = TextEditingController();
|
||||||
|
final TextEditingController alamatController = TextEditingController();
|
||||||
|
final TextEditingController noHpController = TextEditingController();
|
||||||
|
final TextEditingController jenisController = TextEditingController();
|
||||||
|
|
||||||
|
// Form key untuk registrasi donatur
|
||||||
|
final GlobalKey<FormState> registerDonaturFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// Metode untuk registrasi donatur
|
||||||
|
Future<void> registerDonatur() async {
|
||||||
|
print('DEBUG: Memulai proses registrasi donatur');
|
||||||
|
|
||||||
|
if (registerDonaturFormKey.currentState == null) {
|
||||||
|
print('Error: registerDonaturFormKey.currentState adalah null');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Terjadi kesalahan pada form registrasi. Silakan coba lagi.',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registerDonaturFormKey.currentState!.validate()) {
|
||||||
|
print('DEBUG: Validasi form gagal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// Proses registrasi donatur dengan role_id 3
|
||||||
|
await _authProvider.signUpDonatur(
|
||||||
|
email: emailController.text,
|
||||||
|
password: passwordController.text,
|
||||||
|
namaLengkap: namaController.text,
|
||||||
|
alamat: alamatController.text,
|
||||||
|
noHp: noHpController.text,
|
||||||
|
jenis: jenisController.text.isEmpty ? 'Individu' : jenisController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Registrasi donatur berhasil! Silakan login dengan akun Anda.',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bersihkan form
|
||||||
|
clearDonaturRegistrationForm();
|
||||||
|
|
||||||
|
// Arahkan ke halaman login
|
||||||
|
Get.offAllNamed(Routes.login);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error registrasi donatur: $e');
|
||||||
|
|
||||||
|
String errorMessage = 'Gagal melakukan registrasi';
|
||||||
|
|
||||||
|
// Tangani error sesuai jenisnya
|
||||||
|
if (e.toString().contains('email konfirmasi')) {
|
||||||
|
errorMessage =
|
||||||
|
'Gagal mengirim email konfirmasi. Mohon periksa alamat email Anda dan coba lagi nanti.';
|
||||||
|
} else if (e.toString().contains('Email sudah terdaftar')) {
|
||||||
|
errorMessage =
|
||||||
|
'Email sudah terdaftar. Silakan gunakan email lain atau login dengan email tersebut.';
|
||||||
|
} else if (e.toString().contains('weak-password')) {
|
||||||
|
errorMessage =
|
||||||
|
'Password terlalu lemah. Gunakan kombinasi huruf, angka, dan simbol.';
|
||||||
|
} else if (e.toString().contains('invalid-email')) {
|
||||||
|
errorMessage = 'Format email tidak valid.';
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Gagal melakukan registrasi: ${e.toString()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
errorMessage,
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk membersihkan form registrasi donatur
|
||||||
|
void clearDonaturRegistrationForm() {
|
||||||
|
emailController.clear();
|
||||||
|
passwordController.clear();
|
||||||
|
confirmPasswordController.clear();
|
||||||
|
namaController.clear();
|
||||||
|
alamatController.clear();
|
||||||
|
noHpController.clear();
|
||||||
|
jenisController.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
|
|
||||||
class LoginView extends GetView<AuthController> {
|
class LoginView extends GetView<AuthController> {
|
||||||
const LoginView({super.key});
|
const LoginView({super.key});
|
||||||
@ -109,6 +110,39 @@ class LoginView extends GetView<AuthController> {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider()),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child:
|
||||||
|
Text('ATAU', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Register Donatur Button
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => Get.toNamed(Routes.registerDonatur),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
side: const BorderSide(color: Colors.blue),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'DAFTAR SEBAGAI DONATUR',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
214
lib/app/modules/auth/views/register_donatur_view.dart
Normal file
214
lib/app/modules/auth/views/register_donatur_view.dart
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||||
|
|
||||||
|
class RegisterDonaturView extends GetView<AuthController> {
|
||||||
|
const RegisterDonaturView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Daftar Sebagai Donatur'),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: controller.registerDonaturFormKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Logo atau Judul
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Daftar Donatur',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Isi data untuk mendaftar sebagai donatur',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Nama Lengkap
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.namaController,
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Nama Lengkap',
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateDonaturNama,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Email
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Email',
|
||||||
|
prefixIcon: const Icon(Icons.email),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateEmail,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Password
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validatePassword,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Confirm Password
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.confirmPasswordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Konfirmasi Password',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateConfirmPassword,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// No HP
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.noHpController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Nomor HP',
|
||||||
|
prefixIcon: const Icon(Icons.phone),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateDonaturNoHp,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Alamat
|
||||||
|
TextFormField(
|
||||||
|
controller: controller.alamatController,
|
||||||
|
keyboardType: TextInputType.streetAddress,
|
||||||
|
maxLines: 2,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Alamat',
|
||||||
|
prefixIcon: const Icon(Icons.home),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: controller.validateDonaturAlamat,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Jenis Donatur (Dropdown)
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: controller.jenisController.text.isEmpty
|
||||||
|
? 'Individu'
|
||||||
|
: controller.jenisController.text,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Jenis Donatur',
|
||||||
|
prefixIcon: const Icon(Icons.category),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Individu', child: Text('Individu')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Organisasi', child: Text('Organisasi')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Perusahaan', child: Text('Perusahaan')),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'Lainnya', child: Text('Lainnya')),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.jenisController.text = value ?? 'Individu';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Register Button
|
||||||
|
Obx(() => ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.registerDonatur,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? const SpinKitThreeBounce(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'DAFTAR',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Login Link
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('Sudah punya akun?'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.offAllNamed(Routes.login),
|
||||||
|
child: const Text('Masuk'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_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';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_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/penerima_bantuan_controller.dart';
|
||||||
@ -43,6 +44,11 @@ class PetugasDesaBinding extends Bindings {
|
|||||||
() => StokBantuanController(),
|
() => StokBantuanController(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Daftarkan controller riwayat stok
|
||||||
|
Get.lazyPut<RiwayatStokController>(
|
||||||
|
() => RiwayatStokController(),
|
||||||
|
);
|
||||||
|
|
||||||
// Daftarkan controller penitipan bantuan
|
// Daftarkan controller penitipan bantuan
|
||||||
Get.lazyPut<PenitipanBantuanController>(
|
Get.lazyPut<PenitipanBantuanController>(
|
||||||
() => PenitipanBantuanController(),
|
() => PenitipanBantuanController(),
|
||||||
|
@ -439,29 +439,6 @@ class JadwalPenyaluranController extends GetxController {
|
|||||||
.insert(penerimaPenyaluran);
|
.insert(penerimaPenyaluran);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stok bantuan (kurangi dengan total stok yang dibutuhkan)
|
|
||||||
try {
|
|
||||||
// Dapatkan stok saat ini
|
|
||||||
final stokData = await _supabaseService.client
|
|
||||||
.from('stok_bantuan')
|
|
||||||
.select('total_stok')
|
|
||||||
.eq('id', stokBantuanId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (stokData['total_stok'] != null) {
|
|
||||||
final currentStok = stokData['total_stok'].toDouble();
|
|
||||||
final newStok = currentStok - totalStokDibutuhkan;
|
|
||||||
|
|
||||||
// Update stok bantuan dengan nilai baru
|
|
||||||
await _supabaseService.client
|
|
||||||
.from('stok_bantuan')
|
|
||||||
.update({'total_stok': newStok}).eq('id', stokBantuanId);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error updating stok bantuan: $e');
|
|
||||||
// Tidak throw exception di sini karena penyaluran sudah disimpan
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setelah berhasil menambahkan, refresh data
|
// Setelah berhasil menambahkan, refresh data
|
||||||
await loadJadwalData();
|
await loadJadwalData();
|
||||||
await loadPermintaanPenjadwalanData();
|
await loadPermintaanPenjadwalanData();
|
||||||
|
@ -75,15 +75,6 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
|
|
||||||
// Hapus delay dan muat data petugas desa langsung
|
// Hapus delay dan muat data petugas desa langsung
|
||||||
loadAllPetugasDesaData();
|
loadAllPetugasDesaData();
|
||||||
|
|
||||||
// Listener untuk pencarian donatur
|
|
||||||
donaturSearchController.addListener(() {
|
|
||||||
if (donaturSearchController.text.length >= 3) {
|
|
||||||
searchDonatur(donaturSearchController.text);
|
|
||||||
} else {
|
|
||||||
hasilPencarianDonatur.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -441,16 +432,19 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
|
|
||||||
Future<DonaturModel?> getDonaturInfo(String donaturId) async {
|
Future<DonaturModel?> getDonaturInfo(String donaturId) async {
|
||||||
try {
|
try {
|
||||||
// Cek cache terlebih dahulu
|
// Periksa apakah donatur sudah ada di cache
|
||||||
if (donaturCache.containsKey(donaturId)) {
|
if (donaturCache.containsKey(donaturId)) {
|
||||||
return donaturCache[donaturId];
|
return donaturCache[donaturId];
|
||||||
}
|
}
|
||||||
|
|
||||||
final donaturData = await _supabaseService.getDonaturById(donaturId);
|
// Ambil data donatur dari server
|
||||||
if (donaturData != null) {
|
final result = await _supabaseService.getDonaturById(donaturId);
|
||||||
final donatur = DonaturModel.fromJson(donaturData);
|
if (result != null) {
|
||||||
|
final donatur = DonaturModel.fromJson(result);
|
||||||
|
|
||||||
// Simpan ke cache
|
// Simpan ke cache
|
||||||
donaturCache[donaturId] = donatur;
|
donaturCache[donaturId] = donatur;
|
||||||
|
|
||||||
return donatur;
|
return donatur;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -595,16 +589,12 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getPetugasDesaNama(String? petugasDesaId) {
|
String getPetugasDesaNama(String? petugasDesaId) {
|
||||||
print('Petugas Desa ID: $petugasDesaId');
|
|
||||||
if (petugasDesaId == null) {
|
if (petugasDesaId == null) {
|
||||||
return 'Tidak diketahui';
|
return 'Tidak diketahui';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cek apakah data ada di cache
|
// Cek apakah data ada di cache
|
||||||
if (!petugasDesaCache.containsKey(petugasDesaId)) {
|
if (!petugasDesaCache.containsKey(petugasDesaId)) {
|
||||||
print(
|
|
||||||
'Data petugas desa tidak ditemukan di cache untuk ID: $petugasDesaId');
|
|
||||||
// Muat data petugas dan perbarui UI
|
|
||||||
loadPetugasDesaData(petugasDesaId);
|
loadPetugasDesaData(petugasDesaId);
|
||||||
|
|
||||||
// Coba cek lagi setelah pemuatan
|
// Coba cek lagi setelah pemuatan
|
||||||
@ -620,24 +610,18 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
// Sekarang data seharusnya ada di cache
|
// Sekarang data seharusnya ada di cache
|
||||||
// Akses nama dari struktur data petugas_desa
|
// Akses nama dari struktur data petugas_desa
|
||||||
final nama = petugasDesaCache[petugasDesaId]?['nama_lengkap'];
|
final nama = petugasDesaCache[petugasDesaId]?['nama_lengkap'];
|
||||||
print('Nama petugas desa: $nama untuk ID: $petugasDesaId');
|
|
||||||
return nama ?? 'Tidak diketahui';
|
return nama ?? 'Tidak diketahui';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi untuk memuat data petugas desa dan memperbarui UI
|
// Fungsi untuk memuat data petugas desa dan memperbarui UI
|
||||||
Future<void> loadPetugasDesaData(String petugasDesaId) async {
|
Future<void> loadPetugasDesaData(String petugasDesaId) async {
|
||||||
try {
|
try {
|
||||||
print('Memuat data petugas desa untuk ID: $petugasDesaId');
|
|
||||||
final petugasData = await getPetugasDesaInfo(petugasDesaId);
|
final petugasData = await getPetugasDesaInfo(petugasDesaId);
|
||||||
if (petugasData != null) {
|
if (petugasData != null) {
|
||||||
// Data sudah dimasukkan ke cache oleh getPetugasDesaInfo
|
// Data sudah dimasukkan ke cache oleh getPetugasDesaInfo
|
||||||
print('Berhasil memuat data petugas: ${petugasData['nama_lengkap']}');
|
|
||||||
|
|
||||||
// Refresh UI segera
|
// Refresh UI segera
|
||||||
update(['petugas_data']);
|
update(['petugas_data']);
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Gagal mengambil data petugas desa dari server untuk ID: $petugasDesaId');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error saat memuat data petugas desa: $e');
|
print('Error saat memuat data petugas desa: $e');
|
||||||
@ -647,11 +631,9 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
// Fungsi untuk memuat semua data petugas desa yang terkait dengan penitipan
|
// Fungsi untuk memuat semua data petugas desa yang terkait dengan penitipan
|
||||||
void loadAllPetugasDesaData() async {
|
void loadAllPetugasDesaData() async {
|
||||||
try {
|
try {
|
||||||
print('Memuat ulang semua data petugas desa...');
|
|
||||||
for (var item in daftarPenitipan) {
|
for (var item in daftarPenitipan) {
|
||||||
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) {
|
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) {
|
||||||
if (!petugasDesaCache.containsKey(item.petugasDesaId)) {
|
if (!petugasDesaCache.containsKey(item.petugasDesaId)) {
|
||||||
print('Memuat data petugas desa untuk ID: ${item.petugasDesaId}');
|
|
||||||
await getPetugasDesaInfo(item.petugasDesaId);
|
await getPetugasDesaInfo(item.petugasDesaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -669,76 +651,6 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> searchDonatur(String keyword) async {
|
|
||||||
if (keyword.length < 3) {
|
|
||||||
hasilPencarianDonatur.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearchingDonatur.value = true;
|
|
||||||
try {
|
|
||||||
final result = await _supabaseService.searchDonatur(keyword);
|
|
||||||
if (result != null) {
|
|
||||||
hasilPencarianDonatur.value =
|
|
||||||
result.map((data) => DonaturModel.fromJson(data)).toList();
|
|
||||||
} else {
|
|
||||||
hasilPencarianDonatur.clear();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error searching donatur: $e');
|
|
||||||
hasilPencarianDonatur.clear();
|
|
||||||
} finally {
|
|
||||||
isSearchingDonatur.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metode untuk mendapatkan daftar donatur
|
|
||||||
Future<List<DonaturModel>> getDaftarDonatur() async {
|
|
||||||
try {
|
|
||||||
final result = await _supabaseService.getDaftarDonatur();
|
|
||||||
if (result != null) {
|
|
||||||
return result.map((data) => DonaturModel.fromJson(data)).toList();
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
print('Error getting daftar donatur: $e');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> tambahDonatur({
|
|
||||||
required String nama,
|
|
||||||
required String noHp,
|
|
||||||
String? alamat,
|
|
||||||
String? email,
|
|
||||||
String? jenis,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
final donaturData = {
|
|
||||||
'nama_lengkap': nama,
|
|
||||||
'no_hp': noHp,
|
|
||||||
'alamat': alamat,
|
|
||||||
'email': email,
|
|
||||||
'jenis': jenis,
|
|
||||||
'status': 'AKTIF',
|
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return await _supabaseService.tambahDonatur(donaturData);
|
|
||||||
} catch (e) {
|
|
||||||
print('Error adding donatur: $e');
|
|
||||||
Get.snackbar(
|
|
||||||
'Error',
|
|
||||||
'Gagal menambahkan donatur: ${e.toString()}',
|
|
||||||
snackPosition: SnackPosition.TOP,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mendapatkan informasi apakah stok bantuan berupa uang
|
// Mendapatkan informasi apakah stok bantuan berupa uang
|
||||||
bool isStokBantuanUang(String stokBantuanId) {
|
bool isStokBantuanUang(String stokBantuanId) {
|
||||||
if (!stokBantuanMap.containsKey(stokBantuanId)) {
|
if (!stokBantuanMap.containsKey(stokBantuanId)) {
|
||||||
@ -772,4 +684,10 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
print(
|
print(
|
||||||
'Counter updated - Menunggu: $menunggu, Terverifikasi: $terverifikasi, Ditolak: $ditolak');
|
'Counter updated - Menunggu: $menunggu, Terverifikasi: $terverifikasi, Ditolak: $ditolak');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk membersihkan pencarian donatur
|
||||||
|
void resetDonaturSearch() {
|
||||||
|
hasilPencarianDonatur.clear();
|
||||||
|
donaturSearchController.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,305 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/data/models/riwayat_stok_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class RiwayatStokController extends GetxController {
|
||||||
|
final AuthController _authController = Get.find<AuthController>();
|
||||||
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxList<RiwayatStokModel> daftarRiwayatStok = <RiwayatStokModel>[].obs;
|
||||||
|
final RxList<StokBantuanModel> daftarStokBantuan = <StokBantuanModel>[].obs;
|
||||||
|
|
||||||
|
// Filter untuk riwayat stok
|
||||||
|
final RxString filterJenisPerubahan = 'semua'.obs;
|
||||||
|
final RxString filterStokBantuanId = 'semua'.obs;
|
||||||
|
|
||||||
|
// Controller untuk pencarian
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
final RxString searchQuery = ''.obs;
|
||||||
|
|
||||||
|
// Data untuk form penambahan/pengurangan manual
|
||||||
|
final Rx<StokBantuanModel?> selectedStokBantuan = Rx<StokBantuanModel?>(null);
|
||||||
|
final RxDouble jumlah = 0.0.obs;
|
||||||
|
final RxString alasan = ''.obs;
|
||||||
|
final Rx<File?> fotoBukti = Rx<File?>(null);
|
||||||
|
final RxBool isSubmitting = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadRiwayatStok();
|
||||||
|
loadStokBantuan();
|
||||||
|
|
||||||
|
// Listener untuk pencarian
|
||||||
|
searchController.addListener(() {
|
||||||
|
searchQuery.value = searchController.text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
searchController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk memperbarui data saat tab diaktifkan kembali
|
||||||
|
void onTabReactivated() {
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadRiwayatStok() async {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
final String? stokBantuanId = filterStokBantuanId.value != 'semua'
|
||||||
|
? filterStokBantuanId.value
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final String? jenisPerubahan = filterJenisPerubahan.value != 'semua'
|
||||||
|
? filterJenisPerubahan.value
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final riwayatStokData = await _supabaseService.getRiwayatStok(
|
||||||
|
stokBantuanId: stokBantuanId,
|
||||||
|
jenisPerubahan: jenisPerubahan,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (riwayatStokData != null) {
|
||||||
|
daftarRiwayatStok.value = riwayatStokData
|
||||||
|
.map((data) => RiwayatStokModel.fromJson(data))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading riwayat stok data: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memuat data riwayat stok: $e',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadStokBantuan() async {
|
||||||
|
try {
|
||||||
|
final stokBantuanData = await _supabaseService.getStokBantuan();
|
||||||
|
if (stokBantuanData != null) {
|
||||||
|
daftarStokBantuan.value = stokBantuanData
|
||||||
|
.map((data) => StokBantuanModel.fromJson(data))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading stok bantuan data: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshData() async {
|
||||||
|
await loadRiwayatStok();
|
||||||
|
await loadStokBantuan();
|
||||||
|
}
|
||||||
|
|
||||||
|
void filterByJenisPerubahan(String value) {
|
||||||
|
filterJenisPerubahan.value = value;
|
||||||
|
loadRiwayatStok();
|
||||||
|
}
|
||||||
|
|
||||||
|
void filterByStokBantuan(String value) {
|
||||||
|
filterStokBantuanId.value = value;
|
||||||
|
loadRiwayatStok();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RiwayatStokModel> getFilteredRiwayatStok() {
|
||||||
|
if (searchQuery.isEmpty) {
|
||||||
|
return daftarRiwayatStok;
|
||||||
|
}
|
||||||
|
|
||||||
|
return daftarRiwayatStok.where((item) {
|
||||||
|
// Cari berdasarkan nama stok bantuan
|
||||||
|
final stokBantuanMatch = item.stokBantuan != null &&
|
||||||
|
item.stokBantuan!['nama'] != null &&
|
||||||
|
item.stokBantuan!['nama']
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(searchQuery.value.toLowerCase());
|
||||||
|
|
||||||
|
// Cari berdasarkan alasan
|
||||||
|
final alasanMatch = item.alasan != null &&
|
||||||
|
item.alasan!.toLowerCase().contains(searchQuery.value.toLowerCase());
|
||||||
|
|
||||||
|
// Cari berdasarkan sumber
|
||||||
|
final sumberMatch = item.sumber != null &&
|
||||||
|
item.sumber!.toLowerCase().contains(searchQuery.value.toLowerCase());
|
||||||
|
|
||||||
|
return stokBantuanMatch || alasanMatch || sumberMatch;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tambahStokManual() async {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
if (selectedStokBantuan.value == null) {
|
||||||
|
throw Exception('Pilih bantuan terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumlah.value <= 0) {
|
||||||
|
throw Exception('Jumlah harus lebih dari 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alasan.value.isEmpty) {
|
||||||
|
throw Exception('Alasan harus diisi');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fotoBukti.value == null) {
|
||||||
|
throw Exception('Foto bukti harus diupload');
|
||||||
|
}
|
||||||
|
|
||||||
|
final petugasId = _authController.baseUser?.id;
|
||||||
|
if (petugasId == null) {
|
||||||
|
throw Exception('ID petugas tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _supabaseService.tambahStokManual(
|
||||||
|
stokBantuanId: selectedStokBantuan.value!.id!,
|
||||||
|
jumlah: jumlah.value,
|
||||||
|
alasan: alasan.value,
|
||||||
|
fotoBuktiPath: fotoBukti.value!.path,
|
||||||
|
petugasId: petugasId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await refreshData();
|
||||||
|
|
||||||
|
Get.back(); // Tutup dialog
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Stok bantuan berhasil ditambahkan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error menambahkan stok manual: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal menambahkan stok: ${e.toString()}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> kurangiStokManual() async {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
if (selectedStokBantuan.value == null) {
|
||||||
|
throw Exception('Pilih bantuan terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jumlah.value <= 0) {
|
||||||
|
throw Exception('Jumlah harus lebih dari 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alasan.value.isEmpty) {
|
||||||
|
throw Exception('Alasan harus diisi');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fotoBukti.value == null) {
|
||||||
|
throw Exception('Foto bukti harus diupload');
|
||||||
|
}
|
||||||
|
|
||||||
|
final petugasId = _authController.baseUser?.id;
|
||||||
|
if (petugasId == null) {
|
||||||
|
throw Exception('ID petugas tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _supabaseService.kurangiStokManual(
|
||||||
|
stokBantuanId: selectedStokBantuan.value!.id!,
|
||||||
|
jumlah: jumlah.value,
|
||||||
|
alasan: alasan.value,
|
||||||
|
fotoBuktiPath: fotoBukti.value!.path,
|
||||||
|
petugasId: petugasId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await refreshData();
|
||||||
|
|
||||||
|
Get.back(); // Tutup dialog
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Stok bantuan berhasil dikurangi',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error mengurangi stok manual: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal mengurangi stok: ${e.toString()}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedStokBantuan(StokBantuanModel? stokBantuan) {
|
||||||
|
selectedStokBantuan.value = stokBantuan;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setJumlah(double value) {
|
||||||
|
jumlah.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAlasan(String value) {
|
||||||
|
alasan.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickImage() async {
|
||||||
|
try {
|
||||||
|
final pickedFile =
|
||||||
|
await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
fotoBukti.value = File(pickedFile.path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error picking image: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memilih gambar: ${e.toString()}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetForm() {
|
||||||
|
selectedStokBantuan.value = null;
|
||||||
|
jumlah.value = 0.0;
|
||||||
|
alasan.value = '';
|
||||||
|
fotoBukti.value = null;
|
||||||
|
}
|
||||||
|
}
|
@ -249,21 +249,24 @@ class StokBantuanController extends GetxController {
|
|||||||
return filteredList;
|
return filteredList;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metode untuk mendapatkan jumlah stok yang hampir habis (stok <= 10)
|
// Metode untuk mendapatkan jumlah stok yang hampir habis
|
||||||
int getStokHampirHabis() {
|
int getStokHampirHabis() {
|
||||||
return daftarStokBantuan
|
return daftarStokBantuan
|
||||||
.where((stok) => (stok.totalStok ?? 0) <= 10)
|
.where((item) => (item.totalStok ?? 0) <= 10)
|
||||||
.length;
|
.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metode untuk menghitung total dana bantuan
|
// Metode untuk menghitung total dana bantuan dari stok uang
|
||||||
void _hitungTotalDanaBantuan() {
|
void _hitungTotalDanaBantuan() {
|
||||||
double total = 0.0;
|
double total = 0.0;
|
||||||
|
|
||||||
|
// Hitung dari stok yang isUang = true
|
||||||
for (var stok in daftarStokBantuan) {
|
for (var stok in daftarStokBantuan) {
|
||||||
if (stok.isUang == true) {
|
if (stok.isUang == true) {
|
||||||
total += stok.totalStok ?? 0.0;
|
total += stok.totalStok ?? 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDanaBantuan.value = total;
|
totalDanaBantuan.value = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/donatur_model.dart';
|
|
||||||
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
@ -44,11 +43,6 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () => _showTambahPenitipanDialog(context),
|
|
||||||
backgroundColor: AppTheme.primaryColor,
|
|
||||||
child: const Icon(Icons.add, color: Colors.white),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +287,9 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
// Gunakan data donatur dari relasi jika tersedia
|
// Gunakan data donatur dari relasi jika tersedia
|
||||||
final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan';
|
final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan';
|
||||||
|
|
||||||
|
// Cek apakah donatur manual
|
||||||
|
final isDonaturManual = item.donatur?.isManual ?? false;
|
||||||
|
|
||||||
// Debug info
|
// Debug info
|
||||||
print('PenitipanItem - stokBantuanId: ${item.stokBantuanId}');
|
print('PenitipanItem - stokBantuanId: ${item.stokBantuanId}');
|
||||||
|
|
||||||
@ -331,12 +328,43 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Row(
|
||||||
donaturNama,
|
children: [
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
Expanded(
|
||||||
fontWeight: FontWeight.bold,
|
child: Text(
|
||||||
|
donaturNama,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
|
if (isDonaturManual)
|
||||||
|
Tooltip(
|
||||||
|
message: 'Donatur Manual (Diinput oleh petugas desa)',
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(left: 4),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.blue.shade300),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Manual',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
@ -753,771 +781,6 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showTambahPenitipanDialog(BuildContext context) {
|
|
||||||
final formKey = GlobalKey<FormState>();
|
|
||||||
final TextEditingController jumlahController = TextEditingController();
|
|
||||||
final TextEditingController deskripsiController = TextEditingController();
|
|
||||||
|
|
||||||
// Variabel untuk menyimpan nilai yang dipilih
|
|
||||||
final Rx<String?> selectedStokBantuanId = Rx<String?>(null);
|
|
||||||
final Rx<String?> selectedDonaturId = Rx<String?>(null);
|
|
||||||
final Rx<DonaturModel?> selectedDonatur = Rx<DonaturModel?>(null);
|
|
||||||
|
|
||||||
// Reset foto bantuan paths
|
|
||||||
controller.fotoBantuanPaths.clear();
|
|
||||||
controller.donaturSearchController.clear();
|
|
||||||
controller.hasilPencarianDonatur.clear();
|
|
||||||
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Obx(() {
|
|
||||||
// Dapatkan informasi apakah stok bantuan berupa uang
|
|
||||||
bool isUang = false;
|
|
||||||
String satuan = '';
|
|
||||||
if (selectedStokBantuanId.value != null) {
|
|
||||||
isUang =
|
|
||||||
controller.isStokBantuanUang(selectedStokBantuanId.value!);
|
|
||||||
satuan =
|
|
||||||
controller.getKategoriSatuan(selectedStokBantuanId.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Form(
|
|
||||||
key: formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Tambah Manual Penitipan Bantuan',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Pilih kategori bantuan
|
|
||||||
Text(
|
|
||||||
'Jenis Stok Bantuan',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
hint: const Text('Pilih jenis stok bantuan'),
|
|
||||||
value: selectedStokBantuanId.value,
|
|
||||||
items: controller.stokBantuanMap.entries.map((entry) {
|
|
||||||
return DropdownMenuItem<String>(
|
|
||||||
value: entry.key,
|
|
||||||
child: Text(entry.value.nama ?? 'Tidak ada nama'),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
selectedStokBantuanId.value = value;
|
|
||||||
},
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Kategori bantuan harus dipilih';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Jumlah bantuan
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
isUang ? 'Jumlah Uang (Rp)' : 'Jumlah Bantuan',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: jumlahController,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText:
|
|
||||||
isUang ? 'Contoh: 100000' : 'Contoh: 10',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Jumlah harus diisi';
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'Jumlah harus berupa angka';
|
|
||||||
}
|
|
||||||
if (double.parse(value) <= 0) {
|
|
||||||
return 'Jumlah harus lebih dari 0';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (satuan.isNotEmpty && !isUang) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 32),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
satuan,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Donatur (wajib)
|
|
||||||
Text(
|
|
||||||
'Donatur',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (selectedDonatur.value != null) ...[
|
|
||||||
// Tampilkan donatur yang dipilih
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
selectedDonatur.value!.nama ??
|
|
||||||
'Tidak ada nama',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
if (selectedDonatur.value!.noHp != null)
|
|
||||||
Text(selectedDonatur.value!.noHp!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () {
|
|
||||||
selectedDonatur.value = null;
|
|
||||||
selectedDonaturId.value = null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
// Tampilkan pencarian donatur
|
|
||||||
TextFormField(
|
|
||||||
controller: controller.donaturSearchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Cari donatur (min. 3 karakter)',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
suffixIcon: controller.isSearchingDonatur.value
|
|
||||||
? const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
controller.searchDonatur(value);
|
|
||||||
},
|
|
||||||
validator: (value) {
|
|
||||||
if (selectedDonaturId.value == null) {
|
|
||||||
return 'Donatur harus dipilih';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// Hasil pencarian donatur
|
|
||||||
if (controller.hasilPencarianDonatur.isNotEmpty)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(maxHeight: 150),
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount:
|
|
||||||
controller.hasilPencarianDonatur.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final donatur =
|
|
||||||
controller.hasilPencarianDonatur[index];
|
|
||||||
return ListTile(
|
|
||||||
title:
|
|
||||||
Text(donatur.nama ?? 'Tidak ada nama'),
|
|
||||||
subtitle: donatur.noHp != null
|
|
||||||
? Text(donatur.noHp!)
|
|
||||||
: const Text('Tidak ada nomor telepon'),
|
|
||||||
dense: true,
|
|
||||||
onTap: () {
|
|
||||||
selectedDonatur.value = donatur;
|
|
||||||
selectedDonaturId.value = donatur.id;
|
|
||||||
controller.donaturSearchController
|
|
||||||
.clear();
|
|
||||||
controller.hasilPencarianDonatur.clear();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Tombol tambah donatur baru
|
|
||||||
if (controller.donaturSearchController.text.length >=
|
|
||||||
3 &&
|
|
||||||
controller.hasilPencarianDonatur.isEmpty &&
|
|
||||||
!controller.isSearchingDonatur.value)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
_showTambahDonaturDialog(context,
|
|
||||||
(donaturId) {
|
|
||||||
// Callback ketika donatur berhasil ditambahkan
|
|
||||||
controller
|
|
||||||
.getDonaturInfo(donaturId)
|
|
||||||
.then((donatur) {
|
|
||||||
if (donatur != null) {
|
|
||||||
selectedDonatur.value = donatur;
|
|
||||||
selectedDonaturId.value = donatur.id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Tambah Donatur Baru'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16, vertical: 8),
|
|
||||||
foregroundColor: AppTheme.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Deskripsi
|
|
||||||
Text(
|
|
||||||
'Deskripsi',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: deskripsiController,
|
|
||||||
maxLines: 3,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Deskripsi bantuan',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Deskripsi harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Upload foto bantuan
|
|
||||||
Text(
|
|
||||||
'Foto Bantuan',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (controller.fotoBantuanPaths.isEmpty)
|
|
||||||
InkWell(
|
|
||||||
onTap: () => _showPilihSumberFoto(context),
|
|
||||||
child: Container(
|
|
||||||
height: 150,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.camera_alt,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Tambah Foto',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 100,
|
|
||||||
child: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: controller.fotoBantuanPaths.length +
|
|
||||||
1, // +1 untuk tombol tambah
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index ==
|
|
||||||
controller.fotoBantuanPaths.length) {
|
|
||||||
// Tombol tambah foto
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => _showPilihSumberFoto(context),
|
|
||||||
child: Container(
|
|
||||||
width: 100,
|
|
||||||
margin: const EdgeInsets.only(right: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.add_photo_alternate,
|
|
||||||
size: 32,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Tambah',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tampilkan foto yang sudah diambil
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
margin: const EdgeInsets.only(right: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
image: DecorationImage(
|
|
||||||
image: FileImage(File(controller
|
|
||||||
.fotoBantuanPaths[index])),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 4,
|
|
||||||
right: 12,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () =>
|
|
||||||
controller.removeFotoBantuan(index),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Colors.black.withOpacity(0.5),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Tombol aksi
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text('Batal'),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: controller.isUploading.value
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
if (formKey.currentState!.validate()) {
|
|
||||||
if (controller.fotoBantuanPaths.isEmpty) {
|
|
||||||
Get.snackbar(
|
|
||||||
'Error',
|
|
||||||
'Foto bantuan harus diupload',
|
|
||||||
snackPosition: SnackPosition.TOP,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.tambahPenitipanBantuan(
|
|
||||||
stokBantuanId:
|
|
||||||
selectedStokBantuanId.value!,
|
|
||||||
jumlah:
|
|
||||||
double.parse(jumlahController.text),
|
|
||||||
deskripsi: deskripsiController.text,
|
|
||||||
donaturId: selectedDonaturId.value,
|
|
||||||
isUang: isUang,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryColor,
|
|
||||||
),
|
|
||||||
child: controller.isUploading.value
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('Simpan'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPilihSumberFoto(BuildContext context) {
|
|
||||||
Get.bottomSheet(
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Pilih Sumber Foto',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.camera_alt),
|
|
||||||
title: const Text('Kamera'),
|
|
||||||
onTap: () {
|
|
||||||
Get.back();
|
|
||||||
controller.pickFotoBantuan(fromCamera: true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.photo_library),
|
|
||||||
title: const Text('Galeri'),
|
|
||||||
onTap: () {
|
|
||||||
Get.back();
|
|
||||||
controller.pickFotoBantuan(fromCamera: false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showTambahDonaturDialog(
|
|
||||||
BuildContext context, Function(String) onDonaturAdded) {
|
|
||||||
final formKey = GlobalKey<FormState>();
|
|
||||||
final TextEditingController namaController = TextEditingController();
|
|
||||||
final TextEditingController noHpController = TextEditingController();
|
|
||||||
final TextEditingController alamatController = TextEditingController();
|
|
||||||
final TextEditingController emailController = TextEditingController();
|
|
||||||
final TextEditingController jenisController = TextEditingController();
|
|
||||||
|
|
||||||
Get.dialog(
|
|
||||||
Dialog(
|
|
||||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Form(
|
|
||||||
key: formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Tambah Donatur Baru',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Nama donatur
|
|
||||||
Text(
|
|
||||||
'Nama Donatur',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: namaController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Masukkan nama donatur',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Nama donatur harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Telepon
|
|
||||||
Text(
|
|
||||||
'Nomor HP',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: noHpController,
|
|
||||||
keyboardType: TextInputType.phone,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Masukkan nomor HP',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Nomor HP harus diisi';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Jenis (opsional)
|
|
||||||
Text(
|
|
||||||
'Jenis Donatur (Opsional)',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
hint: const Text('Pilih jenis donatur'),
|
|
||||||
value: jenisController.text.isEmpty
|
|
||||||
? null
|
|
||||||
: jenisController.text,
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Individu',
|
|
||||||
child: Text('Individu'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Perusahaan',
|
|
||||||
child: Text('Perusahaan'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<String>(
|
|
||||||
value: 'Organisasi',
|
|
||||||
child: Text('Organisasi'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
jenisController.text = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Alamat (opsional)
|
|
||||||
Text(
|
|
||||||
'Alamat (Opsional)',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: alamatController,
|
|
||||||
maxLines: 2,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Masukkan alamat',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Email (opsional)
|
|
||||||
Text(
|
|
||||||
'Email (Opsional)',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
controller: emailController,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
hintText: 'Masukkan email',
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Tombol aksi
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: const Text('Batal'),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (formKey.currentState!.validate()) {
|
|
||||||
final donaturId = await controller.tambahDonatur(
|
|
||||||
nama: namaController.text,
|
|
||||||
noHp: noHpController.text,
|
|
||||||
alamat: alamatController.text.isEmpty
|
|
||||||
? null
|
|
||||||
: alamatController.text,
|
|
||||||
email: emailController.text.isEmpty
|
|
||||||
? null
|
|
||||||
: emailController.text,
|
|
||||||
jenis: jenisController.text.isEmpty
|
|
||||||
? null
|
|
||||||
: jenisController.text,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (donaturId != null) {
|
|
||||||
Get.back();
|
|
||||||
onDonaturAdded(donaturId);
|
|
||||||
Get.snackbar(
|
|
||||||
'Sukses',
|
|
||||||
'Donatur berhasil ditambahkan',
|
|
||||||
snackPosition: SnackPosition.TOP,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryColor,
|
|
||||||
),
|
|
||||||
child: const Text('Simpan'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tambahkan widget untuk menampilkan waktu terakhir update
|
// Tambahkan widget untuk menampilkan waktu terakhir update
|
||||||
Widget _buildLastUpdateInfo(BuildContext context) {
|
Widget _buildLastUpdateInfo(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
|
@ -5,9 +5,11 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dar
|
|||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/stok_bantuan_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/stok_bantuan_view.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/riwayat_stok_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
|
||||||
|
|
||||||
class PetugasDesaView extends GetView<PetugasDesaController> {
|
class PetugasDesaView extends GetView<PetugasDesaController> {
|
||||||
const PetugasDesaView({super.key});
|
const PetugasDesaView({super.key});
|
||||||
@ -141,6 +143,27 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tampilkan tombol riwayat stok jika tab Stok Bantuan aktif
|
||||||
|
if (activeTab == 4) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Navigasi ke halaman riwayat stok
|
||||||
|
if (!Get.isRegistered<RiwayatStokController>()) {
|
||||||
|
Get.put(RiwayatStokController());
|
||||||
|
}
|
||||||
|
Get.to(() => const RiwayatStokView());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.history),
|
||||||
|
tooltip: 'Riwayat Stok',
|
||||||
|
),
|
||||||
|
notificationButton,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return notificationButton;
|
return notificationButton;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
688
lib/app/modules/petugas_desa/views/riwayat_stok_view.dart
Normal file
688
lib/app/modules/petugas_desa/views/riwayat_stok_view.dart
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:penyaluran_app/app/data/models/riwayat_stok_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok_controller.dart';
|
||||||
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
|
class RiwayatStokView extends GetView<RiwayatStokController> {
|
||||||
|
const RiwayatStokView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Riwayat Stok Bantuan'),
|
||||||
|
//back button
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: controller.refreshData,
|
||||||
|
child: Obx(() => controller.isLoading.value
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildContent(context)),
|
||||||
|
),
|
||||||
|
floatingActionButton: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Tombol untuk mengurangi stok
|
||||||
|
FloatingActionButton.small(
|
||||||
|
onPressed: () => _showStokManualDialog(context, isAddition: false),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
heroTag: 'kurangiStok',
|
||||||
|
child: const Icon(Icons.remove, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Tombol untuk menambah stok
|
||||||
|
FloatingActionButton(
|
||||||
|
onPressed: () => _showStokManualDialog(context, isAddition: true),
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
heroTag: 'tambahStok',
|
||||||
|
child: const Icon(Icons.add, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Heading
|
||||||
|
Text(
|
||||||
|
'Riwayat Stok Bantuan',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Catatan riwayat perubahan stok bantuan',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Filter dan pencarian
|
||||||
|
_buildFilters(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Daftar riwayat stok
|
||||||
|
Obx(() {
|
||||||
|
final filteredRiwayat = controller.getFilteredRiwayatStok();
|
||||||
|
if (filteredRiwayat.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.history,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Belum ada riwayat stok',
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: filteredRiwayat.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final riwayat = filteredRiwayat[index];
|
||||||
|
return _buildRiwayatItem(context, riwayat);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilters(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Pencarian
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.searchController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Cari riwayat stok...',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Filter jenis perubahan
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Jenis Perubahan'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Obx(() => DropdownButtonFormField<String>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
),
|
||||||
|
isExpanded: true,
|
||||||
|
value: controller.filterJenisPerubahan.value,
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem(
|
||||||
|
value: 'semua',
|
||||||
|
child: Text('Semua'),
|
||||||
|
),
|
||||||
|
const DropdownMenuItem(
|
||||||
|
value: 'penambahan',
|
||||||
|
child: Text('Penambahan'),
|
||||||
|
),
|
||||||
|
const DropdownMenuItem(
|
||||||
|
value: 'pengurangan',
|
||||||
|
child: Text('Pengurangan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
controller.filterByJenisPerubahan(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Jenis Bantuan'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Obx(() => DropdownButtonFormField<String>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
),
|
||||||
|
isExpanded: true,
|
||||||
|
value: controller.filterStokBantuanId.value,
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem(
|
||||||
|
value: 'semua',
|
||||||
|
child: Text('Semua'),
|
||||||
|
),
|
||||||
|
...controller.daftarStokBantuan.map((stok) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: stok.id,
|
||||||
|
child: Text(stok.nama ?? '-'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
controller.filterByStokBantuan(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRiwayatItem(BuildContext context, RiwayatStokModel riwayat) {
|
||||||
|
final bool isPenambahan = riwayat.jenisPerubahan == 'penambahan';
|
||||||
|
final stokBantuanNama = riwayat.stokBantuan != null
|
||||||
|
? riwayat.stokBantuan!['nama'] ?? 'Tidak diketahui'
|
||||||
|
: 'Tidak diketahui';
|
||||||
|
final stokBantuanSatuan =
|
||||||
|
riwayat.stokBantuan != null ? riwayat.stokBantuan!['satuan'] ?? '' : '';
|
||||||
|
final sumberLabels = {
|
||||||
|
'penitipan': 'Penitipan',
|
||||||
|
'penyaluran': 'Penyaluran',
|
||||||
|
'manual': 'Manual',
|
||||||
|
};
|
||||||
|
final sumberLabel = sumberLabels[riwayat.sumber] ?? 'Tidak diketahui';
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header: Jumlah dan waktu
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Icon & Jumlah
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isPenambahan ? Colors.green[100] : Colors.red[100],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isPenambahan ? Icons.add : Icons.remove,
|
||||||
|
color: isPenambahan ? Colors.green : Colors.red,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${riwayat.jumlah?.toStringAsFixed(0) ?? '0'} $stokBantuanSatuan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isPenambahan ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// Sumber
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
sumberLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Tanggal
|
||||||
|
Text(
|
||||||
|
riwayat.createdAt != null
|
||||||
|
? DateTimeHelper.formatDateTime(riwayat.createdAt!)
|
||||||
|
: '-',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Nama bantuan
|
||||||
|
Text(
|
||||||
|
stokBantuanNama,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Alasan jika ada
|
||||||
|
if (riwayat.alasan != null && riwayat.alasan!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Alasan: ${riwayat.alasan}',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Foto bukti jika ada
|
||||||
|
if (riwayat.fotoBukti != null && riwayat.fotoBukti!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _showImageDialog(context, riwayat.fotoBukti!),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.photo,
|
||||||
|
color: Colors.blue,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Lihat Bukti',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Petugas
|
||||||
|
if (riwayat.createdBy != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Oleh: ${riwayat.createdBy!['nama_lengkap'] ?? 'Tidak diketahui'}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showImageDialog(BuildContext context, String imageUrl) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
title: const Text('Bukti Foto'),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
InteractiveViewer(
|
||||||
|
panEnabled: true,
|
||||||
|
boundaryMargin: const EdgeInsets.all(16),
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 4,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('Gagal memuat gambar: $error'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
|
||||||
|
// Reset form
|
||||||
|
controller.resetForm();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isAddition ? Icons.add_circle : Icons.remove_circle,
|
||||||
|
color: isAddition ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
isAddition
|
||||||
|
? 'Tambah Stok Manual'
|
||||||
|
: 'Kurangi Stok Manual',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Form
|
||||||
|
// 1. Pilih Bantuan
|
||||||
|
Text(
|
||||||
|
'Pilih Bantuan',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Obx(() => DropdownButtonFormField<StokBantuanModel>(
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 12),
|
||||||
|
),
|
||||||
|
hint: const Text('Pilih bantuan'),
|
||||||
|
value: controller.selectedStokBantuan.value,
|
||||||
|
items: controller.daftarStokBantuan
|
||||||
|
.map((StokBantuanModel stok) {
|
||||||
|
return DropdownMenuItem<StokBantuanModel>(
|
||||||
|
value: stok,
|
||||||
|
child: Text(
|
||||||
|
'${stok.nama} (${stok.totalStok} ${stok.satuan})'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (StokBantuanModel? value) {
|
||||||
|
controller.setSelectedStokBantuan(value);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 2. Jumlah
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Jumlah',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Tampilkan satuan jika bantuan sudah dipilih
|
||||||
|
Obx(() => controller.selectedStokBantuan.value != null
|
||||||
|
? Text(
|
||||||
|
controller.selectedStokBantuan.value!.satuan ??
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 12),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
controller.setJumlah(double.parse(value));
|
||||||
|
} else {
|
||||||
|
controller.setJumlah(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 3. Alasan
|
||||||
|
Text(
|
||||||
|
'Alasan',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 12),
|
||||||
|
hintText: 'Masukkan alasan perubahan stok',
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.setAlasan(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 4. Upload Bukti
|
||||||
|
Text(
|
||||||
|
'Foto Bukti',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: controller.pickImage,
|
||||||
|
child: Container(
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey[400]!),
|
||||||
|
),
|
||||||
|
child: Obx(() {
|
||||||
|
if (controller.fotoBukti.value != null) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Image.file(
|
||||||
|
controller.fotoBukti.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
controller.fotoBukti.value = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.camera_alt,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Pilih Foto',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Obx(() => ElevatedButton(
|
||||||
|
onPressed: controller.isSubmitting.value
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (isAddition) {
|
||||||
|
controller.tambahStokManual();
|
||||||
|
} else {
|
||||||
|
controller.kurangiStokManual();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
isAddition ? Colors.green : Colors.red,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
child: controller.isSubmitting.value
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
isAddition ? 'Tambah Stok' : 'Kurangi Stok',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/views/login_view.dart';
|
import 'package:penyaluran_app/app/modules/auth/views/login_view.dart';
|
||||||
|
import 'package:penyaluran_app/app/modules/auth/views/register_donatur_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart';
|
import 'package:penyaluran_app/app/modules/auth/bindings/auth_binding.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/views/petugas_desa_view.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/views/petugas_desa_view.dart';
|
||||||
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/petugas_desa_binding.dart';
|
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/petugas_desa_binding.dart';
|
||||||
@ -57,6 +58,11 @@ class AppPages {
|
|||||||
page: () => const LoginView(),
|
page: () => const LoginView(),
|
||||||
binding: AuthBinding(),
|
binding: AuthBinding(),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.registerDonatur,
|
||||||
|
page: () => const RegisterDonaturView(),
|
||||||
|
binding: AuthBinding(),
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.wargaDashboard,
|
name: Routes.wargaDashboard,
|
||||||
page: () => WargaView(),
|
page: () => WargaView(),
|
||||||
|
@ -5,6 +5,7 @@ abstract class Routes {
|
|||||||
static const home = _Paths.home;
|
static const home = _Paths.home;
|
||||||
static const login = _Paths.login;
|
static const login = _Paths.login;
|
||||||
static const register = _Paths.register;
|
static const register = _Paths.register;
|
||||||
|
static const registerDonatur = _Paths.registerDonatur;
|
||||||
static const wargaDashboard = _Paths.wargaDashboard;
|
static const wargaDashboard = _Paths.wargaDashboard;
|
||||||
static const wargaPenerimaan = _Paths.wargaPenerimaan;
|
static const wargaPenerimaan = _Paths.wargaPenerimaan;
|
||||||
static const wargaPengaduan = _Paths.wargaPengaduan;
|
static const wargaPengaduan = _Paths.wargaPengaduan;
|
||||||
@ -49,6 +50,7 @@ abstract class _Paths {
|
|||||||
static const home = '/home';
|
static const home = '/home';
|
||||||
static const login = '/login';
|
static const login = '/login';
|
||||||
static const register = '/register';
|
static const register = '/register';
|
||||||
|
static const registerDonatur = '/register-donatur';
|
||||||
static const wargaDashboard = '/warga-dashboard';
|
static const wargaDashboard = '/warga-dashboard';
|
||||||
static const wargaPenerimaan = '/warga-penerimaan';
|
static const wargaPenerimaan = '/warga-penerimaan';
|
||||||
static const wargaPengaduan = '/warga-pengaduan';
|
static const wargaPengaduan = '/warga-pengaduan';
|
||||||
|
@ -834,9 +834,30 @@ class SupabaseService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final petugasDesaId = client.auth.currentUser?.id;
|
final petugasDesaId = client.auth.currentUser?.id;
|
||||||
|
if (petugasDesaId == null) {
|
||||||
|
throw 'ID petugas desa tidak ditemukan';
|
||||||
|
}
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'Verifikasi penitipan dengan ID: $penitipanId oleh petugas desa ID: $petugasDesaId');
|
'Verifikasi penitipan dengan ID: $penitipanId oleh petugas desa ID: $petugasDesaId');
|
||||||
|
|
||||||
|
// 1. Dapatkan data penitipan untuk mendapatkan stok_bantuan_id dan jumlah
|
||||||
|
final response = await client
|
||||||
|
.from('penitipan_bantuan')
|
||||||
|
.select('stok_bantuan_id, jumlah')
|
||||||
|
.eq('id', penitipanId);
|
||||||
|
|
||||||
|
if (response == null || response.isEmpty) {
|
||||||
|
throw 'Data penitipan tidak ditemukan';
|
||||||
|
}
|
||||||
|
|
||||||
|
final penitipanData = response[0];
|
||||||
|
final String stokBantuanId = penitipanData['stok_bantuan_id'];
|
||||||
|
final double jumlah = penitipanData['jumlah'] is int
|
||||||
|
? penitipanData['jumlah'].toDouble()
|
||||||
|
: penitipanData['jumlah'];
|
||||||
|
|
||||||
|
// 2. Update status penitipan menjadi terverifikasi
|
||||||
final updateData = {
|
final updateData = {
|
||||||
'status': 'TERVERIFIKASI',
|
'status': 'TERVERIFIKASI',
|
||||||
'tanggal_verifikasi': DateTime.now().toIso8601String(),
|
'tanggal_verifikasi': DateTime.now().toIso8601String(),
|
||||||
@ -852,7 +873,11 @@ class SupabaseService extends GetxService {
|
|||||||
.update(updateData)
|
.update(updateData)
|
||||||
.eq('id', penitipanId);
|
.eq('id', penitipanId);
|
||||||
|
|
||||||
print('Penitipan berhasil diverifikasi dan data petugas desa disimpan');
|
// 3. Tambahkan ke stok dan catat di riwayat stok
|
||||||
|
await tambahStokDariPenitipan(
|
||||||
|
penitipanId, stokBantuanId, jumlah, petugasDesaId);
|
||||||
|
|
||||||
|
print('Penitipan berhasil diverifikasi dan stok bantuan ditambahkan');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error verifying penitipan: $e');
|
print('Error verifying penitipan: $e');
|
||||||
throw e.toString();
|
throw e.toString();
|
||||||
@ -1591,6 +1616,12 @@ class SupabaseService extends GetxService {
|
|||||||
String? buktiPenerimaan,
|
String? buktiPenerimaan,
|
||||||
String? keterangan}) async {
|
String? keterangan}) async {
|
||||||
try {
|
try {
|
||||||
|
// Periksa petugas ID
|
||||||
|
final petugasId = client.auth.currentUser?.id;
|
||||||
|
if (petugasId == null) {
|
||||||
|
throw Exception('ID petugas tidak ditemukan');
|
||||||
|
}
|
||||||
|
|
||||||
final Map<String, dynamic> updateData = {
|
final Map<String, dynamic> updateData = {
|
||||||
'status_penerimaan': status,
|
'status_penerimaan': status,
|
||||||
};
|
};
|
||||||
@ -1607,11 +1638,34 @@ class SupabaseService extends GetxService {
|
|||||||
updateData['keterangan'] = keterangan;
|
updateData['keterangan'] = keterangan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update status penerimaan
|
||||||
await client
|
await client
|
||||||
.from('penerima_penyaluran')
|
.from('penerima_penyaluran')
|
||||||
.update(updateData)
|
.update(updateData)
|
||||||
.eq('id', penerimaId);
|
.eq('id', penerimaId);
|
||||||
|
|
||||||
|
// Jika status adalah DITERIMA, kurangi stok
|
||||||
|
if (status.toUpperCase() == 'DITERIMA') {
|
||||||
|
// Dapatkan data penerima penyaluran (stok_bantuan_id dan jumlah)
|
||||||
|
final penerimaData = await client
|
||||||
|
.from('penerima_penyaluran')
|
||||||
|
.select('penyaluran_bantuan_id, stok_bantuan_id, jumlah')
|
||||||
|
.eq('id', penerimaId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (penerimaData != null) {
|
||||||
|
final String penyaluranId = penerimaData['penyaluran_bantuan_id'];
|
||||||
|
final String stokBantuanId = penerimaData['stok_bantuan_id'];
|
||||||
|
final double jumlah = penerimaData['jumlah'] is int
|
||||||
|
? penerimaData['jumlah'].toDouble()
|
||||||
|
: penerimaData['jumlah'];
|
||||||
|
|
||||||
|
// Kurangi stok dan catat riwayat
|
||||||
|
await kurangiStokDariPenyaluran(
|
||||||
|
penyaluranId, stokBantuanId, jumlah, petugasId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error updating status penerimaan: $e');
|
print('Error updating status penerimaan: $e');
|
||||||
@ -1840,4 +1894,286 @@ class SupabaseService extends GetxService {
|
|||||||
throw e.toString();
|
throw e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Riwayat Stok methods
|
||||||
|
Future<List<Map<String, dynamic>>?> getRiwayatStok(
|
||||||
|
{String? stokBantuanId, String? jenisPerubahan}) async {
|
||||||
|
try {
|
||||||
|
var filterString = '';
|
||||||
|
if (stokBantuanId != null) {
|
||||||
|
filterString += 'stok_bantuan_id.eq.$stokBantuanId';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jenisPerubahan != null) {
|
||||||
|
filterString += (filterString.isNotEmpty ? ',' : '') +
|
||||||
|
'jenis_perubahan.eq.$jenisPerubahan';
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await client.from('riwayat_stok').select('''
|
||||||
|
*,
|
||||||
|
stok_bantuan:stok_bantuan_id(*),
|
||||||
|
petugas_desa:created_by_id(*)
|
||||||
|
''').order('created_at', ascending: false);
|
||||||
|
|
||||||
|
var result = response;
|
||||||
|
if (filterString.isNotEmpty) {
|
||||||
|
// Menerapkan filter secara manual karena response sudah berupa List
|
||||||
|
result = result.where((item) {
|
||||||
|
if (stokBantuanId != null &&
|
||||||
|
item['stok_bantuan_id'] != stokBantuanId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (jenisPerubahan != null &&
|
||||||
|
item['jenis_perubahan'] != jenisPerubahan) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting riwayat stok: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk mencatat penambahan stok dari penitipan
|
||||||
|
Future<void> tambahStokDariPenitipan(String penitipanId, String stokBantuanId,
|
||||||
|
double jumlah, String petugasId) async {
|
||||||
|
try {
|
||||||
|
// 1. Update stok bantuan - tambahkan jumlah
|
||||||
|
final stokBantuanResponse = await client
|
||||||
|
.from('stok_bantuan')
|
||||||
|
.select('total_stok')
|
||||||
|
.eq('id', stokBantuanId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Konversi total_stok ke double terlepas dari apakah itu int atau double
|
||||||
|
double currentStok = 0.0;
|
||||||
|
if (stokBantuanResponse['total_stok'] != null) {
|
||||||
|
if (stokBantuanResponse['total_stok'] is int) {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'].toDouble();
|
||||||
|
} else {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double newStok = currentStok + jumlah;
|
||||||
|
|
||||||
|
// Update stok bantuan
|
||||||
|
await client.from('stok_bantuan').update({
|
||||||
|
'total_stok': newStok,
|
||||||
|
'updated_at': DateTime.now().toIso8601String()
|
||||||
|
}).eq('id', stokBantuanId);
|
||||||
|
|
||||||
|
// 2. Catat riwayat penambahan
|
||||||
|
await client.from('riwayat_stok').insert({
|
||||||
|
'stok_bantuan_id': stokBantuanId,
|
||||||
|
'jenis_perubahan': 'penambahan',
|
||||||
|
'jumlah': jumlah,
|
||||||
|
'sumber': 'penitipan',
|
||||||
|
'id_referensi': penitipanId,
|
||||||
|
'created_by_id': petugasId,
|
||||||
|
'created_at': DateTime.now().toIso8601String()
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Stok berhasil ditambahkan dari penitipan');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error adding stok from penitipan: $e');
|
||||||
|
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk mencatat pengurangan stok dari penyaluran
|
||||||
|
Future<void> kurangiStokDariPenyaluran(String penyaluranId,
|
||||||
|
String stokBantuanId, double jumlah, String petugasId) async {
|
||||||
|
try {
|
||||||
|
// 1. Update stok bantuan - kurangi jumlah
|
||||||
|
final stokBantuanResponse = await client
|
||||||
|
.from('stok_bantuan')
|
||||||
|
.select('total_stok')
|
||||||
|
.eq('id', stokBantuanId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Konversi total_stok ke double terlepas dari apakah itu int atau double
|
||||||
|
double currentStok = 0.0;
|
||||||
|
if (stokBantuanResponse['total_stok'] != null) {
|
||||||
|
if (stokBantuanResponse['total_stok'] is int) {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'].toDouble();
|
||||||
|
} else {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi stok cukup
|
||||||
|
if (currentStok < jumlah) {
|
||||||
|
throw Exception('Stok tidak mencukupi untuk pengurangan');
|
||||||
|
}
|
||||||
|
|
||||||
|
double newStok = currentStok - jumlah;
|
||||||
|
|
||||||
|
// Update stok bantuan
|
||||||
|
await client.from('stok_bantuan').update({
|
||||||
|
'total_stok': newStok,
|
||||||
|
'updated_at': DateTime.now().toIso8601String()
|
||||||
|
}).eq('id', stokBantuanId);
|
||||||
|
|
||||||
|
// 2. Catat riwayat pengurangan
|
||||||
|
await client.from('riwayat_stok').insert({
|
||||||
|
'stok_bantuan_id': stokBantuanId,
|
||||||
|
'jenis_perubahan': 'pengurangan',
|
||||||
|
'jumlah': jumlah,
|
||||||
|
'sumber': 'penyaluran',
|
||||||
|
'id_referensi': penyaluranId,
|
||||||
|
'created_by_id': petugasId,
|
||||||
|
'created_at': DateTime.now().toIso8601String()
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Stok berhasil dikurangi dari penyaluran');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error reducing stok from penyaluran: $e');
|
||||||
|
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk penambahan stok manual oleh petugas
|
||||||
|
Future<void> tambahStokManual({
|
||||||
|
required String stokBantuanId,
|
||||||
|
required double jumlah,
|
||||||
|
required String alasan,
|
||||||
|
required String fotoBuktiPath,
|
||||||
|
required String petugasId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. Upload foto bukti jika disediakan
|
||||||
|
String fotoBuktiUrl = '';
|
||||||
|
if (fotoBuktiPath.isNotEmpty) {
|
||||||
|
final String fileName =
|
||||||
|
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
||||||
|
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||||
|
fileName,
|
||||||
|
File(fotoBuktiPath),
|
||||||
|
fileOptions:
|
||||||
|
const FileOptions(cacheControl: '3600', upsert: false),
|
||||||
|
);
|
||||||
|
fotoBuktiUrl = client.storage.from('stok_bukti').getPublicUrl(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update stok bantuan - tambahkan jumlah
|
||||||
|
final stokBantuanResponse = await client
|
||||||
|
.from('stok_bantuan')
|
||||||
|
.select('total_stok')
|
||||||
|
.eq('id', stokBantuanId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Konversi total_stok ke double terlepas dari apakah itu int atau double
|
||||||
|
double currentStok = 0.0;
|
||||||
|
if (stokBantuanResponse['total_stok'] != null) {
|
||||||
|
if (stokBantuanResponse['total_stok'] is int) {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'].toDouble();
|
||||||
|
} else {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double newStok = currentStok + jumlah;
|
||||||
|
|
||||||
|
// Update stok bantuan
|
||||||
|
await client.from('stok_bantuan').update({
|
||||||
|
'total_stok': newStok,
|
||||||
|
'updated_at': DateTime.now().toIso8601String()
|
||||||
|
}).eq('id', stokBantuanId);
|
||||||
|
|
||||||
|
// 3. Catat riwayat penambahan
|
||||||
|
await client.from('riwayat_stok').insert({
|
||||||
|
'stok_bantuan_id': stokBantuanId,
|
||||||
|
'jenis_perubahan': 'penambahan',
|
||||||
|
'jumlah': jumlah,
|
||||||
|
'sumber': 'manual',
|
||||||
|
'alasan': alasan,
|
||||||
|
'foto_bukti': fotoBuktiUrl,
|
||||||
|
'created_by_id': petugasId,
|
||||||
|
'created_at': DateTime.now().toIso8601String()
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Stok berhasil ditambahkan secara manual');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error adding stok manually: $e');
|
||||||
|
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk pengurangan stok manual oleh petugas
|
||||||
|
Future<void> kurangiStokManual({
|
||||||
|
required String stokBantuanId,
|
||||||
|
required double jumlah,
|
||||||
|
required String alasan,
|
||||||
|
required String fotoBuktiPath,
|
||||||
|
required String petugasId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. Validasi stok yang tersedia
|
||||||
|
final stokBantuanResponse = await client
|
||||||
|
.from('stok_bantuan')
|
||||||
|
.select('total_stok')
|
||||||
|
.eq('id', stokBantuanId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Konversi total_stok ke double terlepas dari apakah itu int atau double
|
||||||
|
double currentStok = 0.0;
|
||||||
|
if (stokBantuanResponse['total_stok'] != null) {
|
||||||
|
if (stokBantuanResponse['total_stok'] is int) {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'].toDouble();
|
||||||
|
} else {
|
||||||
|
currentStok = stokBantuanResponse['total_stok'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi stok cukup
|
||||||
|
if (currentStok < jumlah) {
|
||||||
|
throw Exception('Stok tidak mencukupi untuk pengurangan');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upload foto bukti jika disediakan
|
||||||
|
String fotoBuktiUrl = '';
|
||||||
|
if (fotoBuktiPath.isNotEmpty) {
|
||||||
|
final String fileName =
|
||||||
|
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
|
||||||
|
final fileResponse = await client.storage.from('stok_bukti').upload(
|
||||||
|
fileName,
|
||||||
|
File(fotoBuktiPath),
|
||||||
|
fileOptions:
|
||||||
|
const FileOptions(cacheControl: '3600', upsert: false),
|
||||||
|
);
|
||||||
|
fotoBuktiUrl = client.storage.from('stok_bukti').getPublicUrl(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update stok bantuan - kurangi jumlah
|
||||||
|
double newStok = currentStok - jumlah;
|
||||||
|
|
||||||
|
// Update stok bantuan
|
||||||
|
await client.from('stok_bantuan').update({
|
||||||
|
'total_stok': newStok,
|
||||||
|
'updated_at': DateTime.now().toIso8601String()
|
||||||
|
}).eq('id', stokBantuanId);
|
||||||
|
|
||||||
|
// 4. Catat riwayat pengurangan
|
||||||
|
await client.from('riwayat_stok').insert({
|
||||||
|
'stok_bantuan_id': stokBantuanId,
|
||||||
|
'jenis_perubahan': 'pengurangan',
|
||||||
|
'jumlah': jumlah,
|
||||||
|
'sumber': 'manual',
|
||||||
|
'alasan': alasan,
|
||||||
|
'foto_bukti': fotoBuktiUrl,
|
||||||
|
'created_by_id': petugasId,
|
||||||
|
'created_at': DateTime.now().toIso8601String()
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Stok berhasil dikurangi secara manual');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error reducing stok manually: $e');
|
||||||
|
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import flutter_secure_storage_macos
|
|||||||
import open_file_mac
|
import open_file_mac
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@ -22,5 +23,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
22
migrations/add_is_manual_to_donatur.sql
Normal file
22
migrations/add_is_manual_to_donatur.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- Migrasi untuk menambahkan kolom is_manual pada tabel donatur
|
||||||
|
-- Jalankan melalui SQL Editor di Supabase
|
||||||
|
|
||||||
|
-- Tambahkan kolom is_manual jika belum ada
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'donatur'
|
||||||
|
AND column_name = 'is_manual'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE donatur ADD COLUMN is_manual BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Tambahkan indeks untuk mempercepat pencarian donatur manual
|
||||||
|
CREATE INDEX idx_donatur_is_manual ON donatur(is_manual);
|
||||||
|
|
||||||
|
-- Tambahkan komentar pada kolom
|
||||||
|
COMMENT ON COLUMN donatur.is_manual IS 'Flag untuk menandai donatur yang dibuat secara manual oleh petugas desa';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
60
panduan_admin_supabase.md
Normal file
60
panduan_admin_supabase.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Panduan Admin Supabase: Mengatasi Masalah Konfirmasi Email
|
||||||
|
|
||||||
|
## Masalah
|
||||||
|
|
||||||
|
Aplikasi Penyaluran mengalami error saat registrasi donatur:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error sending confirmation mail
|
||||||
|
```
|
||||||
|
|
||||||
|
Ini terjadi karena Supabase tidak dapat mengirim email konfirmasi, yang bisa disebabkan oleh:
|
||||||
|
|
||||||
|
1. SMTP belum dikonfigurasi dengan benar
|
||||||
|
2. Email template tidak valid
|
||||||
|
3. Konfigurasi DNS untuk domain email tidak benar
|
||||||
|
|
||||||
|
## Solusi 1: Menonaktifkan Konfirmasi Email (Paling Mudah)
|
||||||
|
|
||||||
|
1. Login ke dashboard Supabase project Anda
|
||||||
|
2. Pilih tab "Authentication" di menu sebelah kiri
|
||||||
|
3. Pilih "Email Templates"
|
||||||
|
4. Pada tab "Confirmation" nonaktifkan toggle "Enable email confirmations"
|
||||||
|
5. Klik "Save"
|
||||||
|
|
||||||
|
Dengan menonaktifkan ini, pengguna dapat langsung login setelah registrasi tanpa perlu konfirmasi email.
|
||||||
|
|
||||||
|
## Solusi 2: Menggunakan Auto-Confirm SQL Function (Lebih Aman)
|
||||||
|
|
||||||
|
Jika Anda ingin tetap menyimpan riwayat kapan email dikonfirmasi, tapi tidak ingin bergantung pada email konfirmasi aktual, ikuti langkah berikut:
|
||||||
|
|
||||||
|
1. Login ke dashboard Supabase project Anda
|
||||||
|
2. Pilih tab "SQL Editor" di menu sebelah kiri
|
||||||
|
3. Buat SQL query baru
|
||||||
|
4. Salin dan tempel kode dari file:
|
||||||
|
```
|
||||||
|
supabase/migrations/20230601000000_disable_email_verification.sql
|
||||||
|
```
|
||||||
|
5. Jalankan SQL dengan klik tombol "Run"
|
||||||
|
|
||||||
|
Setelah menjalankan SQL ini, semua pengguna baru akan otomatis dikonfirmasi emailnya tanpa perlu mengklik link konfirmasi.
|
||||||
|
|
||||||
|
## Solusi 3: Mengkonfigurasi SMTP dengan Benar (Solusi Permanen)
|
||||||
|
|
||||||
|
Untuk mengatasi masalah secara permanen dan tetap menggunakan konfirmasi email:
|
||||||
|
|
||||||
|
1. Login ke dashboard Supabase project Anda
|
||||||
|
2. Pilih tab "Settings" di menu sebelah kiri
|
||||||
|
3. Pilih "Auth" dan scroll ke bagian "SMTP Settings"
|
||||||
|
4. Isi dengan informasi SMTP yang valid:
|
||||||
|
- Host: (mis. smtp.gmail.com)
|
||||||
|
- Port: (mis. 587 untuk TLS)
|
||||||
|
- Username: email@domain.com
|
||||||
|
- Password: [password SMTP Anda]
|
||||||
|
- Sender Name: Penyaluran App
|
||||||
|
- Sender Email: noreply@yourdomain.com
|
||||||
|
5. Klik "Save" dan tes konfigurasi
|
||||||
|
|
||||||
|
## Bantuan Lebih Lanjut
|
||||||
|
|
||||||
|
Jika masih mengalami masalah, silakan hubungi tim pengembang atau lihat dokumentasi Supabase di [supabase.com/docs](https://supabase.com/docs).
|
70
panduan_donatur_manual.md
Normal file
70
panduan_donatur_manual.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Panduan Penggunaan Donatur Manual
|
||||||
|
|
||||||
|
## Pengantar
|
||||||
|
|
||||||
|
Fitur Donatur Manual memungkinkan petugas desa untuk menambahkan donatur dan penitipan bantuan secara manual, tanpa mengharuskan donatur memiliki akun login di sistem. Fitur ini berguna untuk donatur yang menyumbang langsung atau yang tidak memiliki akses internet.
|
||||||
|
|
||||||
|
## Cara Menggunakan
|
||||||
|
|
||||||
|
### Langkah 1: Menambahkan Donatur Manual
|
||||||
|
|
||||||
|
1. Buka halaman "Penitipan Bantuan"
|
||||||
|
2. Klik tombol "+" di pojok kanan bawah untuk menambah penitipan
|
||||||
|
3. Pada form penitipan, pilih "Tambah Donatur Baru"
|
||||||
|
4. Isi form donatur dengan data donatur:
|
||||||
|
- Nama Donatur (wajib)
|
||||||
|
- Nomor HP (wajib)
|
||||||
|
- Jenis Donatur (opsional)
|
||||||
|
- Alamat (opsional)
|
||||||
|
- Email (opsional)
|
||||||
|
- Deskripsi (opsional)
|
||||||
|
5. Pastikan opsi "Donatur Manual (tanpa akun login)" dicentang
|
||||||
|
6. Klik "Simpan Donatur Manual"
|
||||||
|
|
||||||
|
### Langkah 2: Menyelesaikan Penitipan Bantuan
|
||||||
|
|
||||||
|
1. Setelah donatur tersimpan, lengkapi form penitipan bantuan:
|
||||||
|
- Pilih jenis stok bantuan
|
||||||
|
- Masukkan jumlah bantuan
|
||||||
|
- Tambahkan deskripsi dan foto bantuan
|
||||||
|
2. Klik "Simpan" untuk menyelesaikan proses penitipan
|
||||||
|
|
||||||
|
## Perbedaan Donatur Manual vs Donatur Reguler
|
||||||
|
|
||||||
|
| Fitur | Donatur Manual | Donatur Reguler |
|
||||||
|
| ------------------------- | ----------------- | ------------------ |
|
||||||
|
| Login ke sistem | ❌ Tidak bisa | ✅ Bisa |
|
||||||
|
| Ditandai khusus di UI | ✅ Label "Manual" | ❌ Tidak ada label |
|
||||||
|
| Mengelola profil sendiri | ❌ Tidak bisa | ✅ Bisa |
|
||||||
|
| Melihat riwayat penitipan | ❌ Tidak bisa | ✅ Bisa |
|
||||||
|
| Menerima notifikasi | ❌ Tidak bisa | ✅ Bisa |
|
||||||
|
|
||||||
|
## Keterbatasan
|
||||||
|
|
||||||
|
- Donatur manual tidak dapat login ke sistem
|
||||||
|
- Donatur manual tidak menerima notifikasi penitipan
|
||||||
|
- Profil donatur manual hanya dapat diubah oleh petugas desa
|
||||||
|
|
||||||
|
## Konversi ke Donatur Reguler
|
||||||
|
|
||||||
|
Jika suatu saat donatur manual ingin memiliki akun resmi:
|
||||||
|
|
||||||
|
1. Donatur perlu mendaftar secara normal melalui halaman registrasi
|
||||||
|
2. Petugas desa perlu memperbarui semua penitipan bantuan yang lama untuk merujuk ke akun donatur yang baru
|
||||||
|
|
||||||
|
## Keamanan & Privasi
|
||||||
|
|
||||||
|
- Meskipun donatur manual tidak memiliki akun login, data mereka tetap aman tersimpan di sistem
|
||||||
|
- Pastikan mendapatkan izin dari donatur untuk menyimpan informasi mereka
|
||||||
|
- Jelaskan kepada donatur tentang bagaimana data mereka akan digunakan
|
||||||
|
|
||||||
|
## Pertanyaan Umum
|
||||||
|
|
||||||
|
1. **Apakah donatur manual dapat digunakan untuk semua jenis penitipan?**
|
||||||
|
Ya, donatur manual dapat digunakan untuk semua jenis penitipan bantuan.
|
||||||
|
|
||||||
|
2. **Bagaimana cara mengidentifikasi donatur manual?**
|
||||||
|
Donatur manual ditandai dengan label "Manual" di sebelah nama mereka.
|
||||||
|
|
||||||
|
3. **Apakah donatur manual dapat dihapus?**
|
||||||
|
Ya, data donatur manual dapat dihapus oleh petugas desa jika diperlukan.
|
112
pubspec.lock
112
pubspec.lock
@ -81,6 +81,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -209,11 +233,27 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
version: "0.9.3+4"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -557,6 +597,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
open_file:
|
open_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -866,6 +914,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.5"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -946,6 +1042,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "28.2.12"
|
version: "28.2.12"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1050,6 +1154,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
|
uuid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_graphics:
|
vector_graphics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -91,6 +91,9 @@ dependencies:
|
|||||||
# Package untuk membuka file
|
# Package untuk membuka file
|
||||||
open_file: ^3.3.2
|
open_file: ^3.3.2
|
||||||
percent_indicator: ^4.2.4
|
percent_indicator: ^4.2.4
|
||||||
|
uuid: ^4.5.1
|
||||||
|
# Library untuk cached image
|
||||||
|
cached_network_image: ^3.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
39
supabase/auto_confirm_function.sql
Normal file
39
supabase/auto_confirm_function.sql
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
-- Fungsi untuk otomatis mengkonfirmasi email pengguna
|
||||||
|
-- File ini harus dijalankan dengan akses SQL admin di Supabase
|
||||||
|
|
||||||
|
-- 1. Buat fungsi untuk update email_confirmed_at
|
||||||
|
CREATE OR REPLACE FUNCTION public.auto_confirm_user(user_id UUID)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Update email_confirmed_at menjadi waktu saat ini
|
||||||
|
UPDATE auth.users
|
||||||
|
SET email_confirmed_at = NOW()
|
||||||
|
WHERE id = user_id AND email_confirmed_at IS NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 2. Buat trigger untuk otomatis mengkonfirmasi email saat user baru dibuat
|
||||||
|
CREATE OR REPLACE FUNCTION public.trigger_auto_confirm_email()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Jika user baru dibuat dan emailnya belum dikonfirmasi, konfirmasi otomatis
|
||||||
|
UPDATE auth.users
|
||||||
|
SET email_confirmed_at = NOW()
|
||||||
|
WHERE id = NEW.id AND email_confirmed_at IS NULL;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 3. Pasang trigger pada tabel auth.users
|
||||||
|
DROP TRIGGER IF EXISTS auto_confirm_email_trigger ON auth.users;
|
||||||
|
CREATE TRIGGER auto_confirm_email_trigger
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.trigger_auto_confirm_email();
|
||||||
|
|
||||||
|
-- Catatan penggunaan:
|
||||||
|
-- 1. Jalankan SQL ini di SQL Editor Supabase sebagai admin
|
||||||
|
-- 2. Setelah dijalankan, semua pengguna baru akan otomatis dikonfirmasi emailnya
|
||||||
|
-- 3. Untuk mengkonfirmasi email pengguna yang sudah ada, jalankan:
|
||||||
|
-- SELECT auto_confirm_user('user-id-disini');
|
@ -0,0 +1,51 @@
|
|||||||
|
-- Migration: Disable Email Verification
|
||||||
|
-- Eksekusi file ini di SQL Editor Supabase untuk mematikan konfirmasi email
|
||||||
|
|
||||||
|
-- 1. Tambahkan fungsi untuk auto konfirmasi email
|
||||||
|
CREATE OR REPLACE FUNCTION public.auto_confirm_user(user_id UUID)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Update email_confirmed_at menjadi waktu saat ini
|
||||||
|
UPDATE auth.users
|
||||||
|
SET email_confirmed_at = NOW()
|
||||||
|
WHERE id = user_id AND email_confirmed_at IS NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 2. Konfirmasi semua user yang belum dikonfirmasi
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
user_record RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR user_record IN SELECT id FROM auth.users WHERE email_confirmed_at IS NULL
|
||||||
|
LOOP
|
||||||
|
PERFORM auto_confirm_user(user_record.id);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 3. Tambahkan trigger untuk auto konfirmasi user baru
|
||||||
|
CREATE OR REPLACE FUNCTION public.trigger_auto_confirm_email()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Auto-konfirmasi email user baru
|
||||||
|
UPDATE auth.users
|
||||||
|
SET email_confirmed_at = NOW()
|
||||||
|
WHERE id = NEW.id AND email_confirmed_at IS NULL;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 4. Pasang trigger pada tabel auth.users
|
||||||
|
DROP TRIGGER IF EXISTS auto_confirm_email_trigger ON auth.users;
|
||||||
|
CREATE TRIGGER auto_confirm_email_trigger
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.trigger_auto_confirm_email();
|
||||||
|
|
||||||
|
-- Informasi untuk pengguna:
|
||||||
|
-- Query ini akan:
|
||||||
|
-- 1. Membuat fungsi untuk auto-konfirmasi user
|
||||||
|
-- 2. Mengkonfirmasi semua user yang belum dikonfirmasi
|
||||||
|
-- 3. Membuat trigger untuk auto-konfirmasi semua user baru
|
Reference in New Issue
Block a user