diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e43eafe..78264b8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,10 @@ + + + + + + UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription + Aplikasi memerlukan akses kamera untuk mengambil foto bukti serah terima + NSPhotoLibraryUsageDescription + Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima + NSMicrophoneUsageDescription + Aplikasi memerlukan akses mikrofon untuk merekam video diff --git a/lib/app/data/models/donatur_model.dart b/lib/app/data/models/donatur_model.dart index 5c56774..e80c20e 100644 --- a/lib/app/data/models/donatur_model.dart +++ b/lib/app/data/models/donatur_model.dart @@ -38,7 +38,7 @@ class DonaturModel { email: json["email"], jenis: json["jenis"], deskripsi: json["deskripsi"], - status: json["status"], + status: json["status"] ?? 'AKTIF', createdAt: json["created_at"] != null ? DateTime.parse(json["created_at"]) : null, @@ -55,7 +55,7 @@ class DonaturModel { "email": email, "jenis": jenis, "deskripsi": deskripsi, - "status": status, + "status": status ?? 'AKTIF', "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; diff --git a/lib/app/data/models/kategori_bantuan_model.dart b/lib/app/data/models/kategori_bantuan_model.dart new file mode 100644 index 0000000..ff137ee --- /dev/null +++ b/lib/app/data/models/kategori_bantuan_model.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; + +class KategoriBantuanModel { + final String? id; + final String? nama; + final String? deskripsi; + final String? satuan; + final DateTime? createdAt; + final DateTime? updatedAt; + + KategoriBantuanModel({ + this.id, + this.nama, + this.deskripsi, + this.satuan, + this.createdAt, + this.updatedAt, + }); + + factory KategoriBantuanModel.fromRawJson(String str) => + KategoriBantuanModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory KategoriBantuanModel.fromJson(Map json) => + KategoriBantuanModel( + id: json["id"], + nama: json["nama"], + deskripsi: json["deskripsi"], + satuan: json["satuan"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, + ); + + Map toJson() => { + "id": id, + "nama": nama, + "deskripsi": deskripsi, + "satuan": satuan, + "created_at": createdAt?.toIso8601String(), + "updated_at": updatedAt?.toIso8601String(), + }; +} diff --git a/lib/app/data/models/penitipan_bantuan_model.dart b/lib/app/data/models/penitipan_bantuan_model.dart index 7e05371..75f42af 100644 --- a/lib/app/data/models/penitipan_bantuan_model.dart +++ b/lib/app/data/models/penitipan_bantuan_model.dart @@ -1,36 +1,46 @@ import 'dart:convert'; +import 'package:penyaluran_app/app/data/models/donatur_model.dart'; +import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart'; class PenitipanBantuanModel { final String? id; final String? donaturId; - final String? bentukBantuanId; - final String? nama; + final String? stokBantuanId; final double? jumlah; - final String? satuan; final String? deskripsi; final String? status; final String? alasanPenolakan; - final List? gambarUrls; + final List? fotoBantuan; final DateTime? tanggalPenitipan; final DateTime? tanggalVerifikasi; final DateTime? createdAt; final DateTime? updatedAt; + final DateTime? tanggalKadaluarsa; + final String? petugasDesaId; + final String? fotoBuktiSerahTerima; + final String? sumberBantuanId; + final DonaturModel? donatur; + final KategoriBantuanModel? kategoriBantuan; PenitipanBantuanModel({ this.id, this.donaturId, - this.bentukBantuanId, - this.nama, + this.stokBantuanId, this.jumlah, - this.satuan, this.deskripsi, this.status, this.alasanPenolakan, - this.gambarUrls, + this.fotoBantuan, this.tanggalPenitipan, this.tanggalVerifikasi, this.createdAt, this.updatedAt, + this.tanggalKadaluarsa, + this.petugasDesaId, + this.fotoBuktiSerahTerima, + this.sumberBantuanId, + this.donatur, + this.kategoriBantuan, }); factory PenitipanBantuanModel.fromRawJson(String str) => @@ -42,16 +52,14 @@ class PenitipanBantuanModel { PenitipanBantuanModel( id: json["id"], donaturId: json["donatur_id"], - bentukBantuanId: json["bentuk_bantuan_id"], - nama: json["nama"], + stokBantuanId: json["stok_bantuan_id"], jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, - satuan: json["satuan"], deskripsi: json["deskripsi"], status: json["status"], alasanPenolakan: json["alasan_penolakan"], - gambarUrls: json["gambar_urls"] == null + fotoBantuan: json["foto_bantuan"] == null ? null - : List.from(json["gambar_urls"].map((x) => x)), + : List.from(json["foto_bantuan"].map((x) => x)), tanggalPenitipan: json["tanggal_penitipan"] != null ? DateTime.parse(json["tanggal_penitipan"]) : null, @@ -64,24 +72,38 @@ class PenitipanBantuanModel { updatedAt: json["updated_at"] != null ? DateTime.parse(json["updated_at"]) : null, + tanggalKadaluarsa: json["tanggal_kadaluarsa"] != null + ? DateTime.parse(json["tanggal_kadaluarsa"]) + : null, + petugasDesaId: json["petugas_desa_id"], + fotoBuktiSerahTerima: json["foto_bukti_serah_terima"], + sumberBantuanId: json["sumber_bantuan_id"], + donatur: json["donatur"] != null + ? DonaturModel.fromJson(json["donatur"]) + : null, + kategoriBantuan: json["kategori_bantuan"] != null + ? KategoriBantuanModel.fromJson(json["kategori_bantuan"]) + : null, ); Map toJson() => { "id": id, "donatur_id": donaturId, - "bentuk_bantuan_id": bentukBantuanId, - "nama": nama, + "stok_bantuan_id": stokBantuanId, "jumlah": jumlah, - "satuan": satuan, "deskripsi": deskripsi, "status": status, "alasan_penolakan": alasanPenolakan, - "gambar_urls": gambarUrls == null + "foto_bantuan": fotoBantuan == null ? null - : List.from(gambarUrls!.map((x) => x)), + : List.from(fotoBantuan!.map((x) => x)), "tanggal_penitipan": tanggalPenitipan?.toIso8601String(), "tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(), "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), + "tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(), + "petugas_desa_id": petugasDesaId, + "foto_bukti_serah_terima": fotoBuktiSerahTerima, + "sumber_bantuan_id": sumberBantuanId, }; } diff --git a/lib/app/data/models/petugas_desa_model.dart b/lib/app/data/models/petugas_desa_model.dart index ba14ca6..8d90b9e 100644 --- a/lib/app/data/models/petugas_desa_model.dart +++ b/lib/app/data/models/petugas_desa_model.dart @@ -1,33 +1,25 @@ import 'dart:convert'; class PetugasDesaModel { - final String id; - final String nama; - final String? alamat; + final String? id; + final String? nama; + final String? alamatLengkap; final String? noTelp; final String? email; final String? jabatan; - final String? desa; - final String? kecamatan; - final String? kabupaten; - final String? provinsi; - final String? userId; // Referensi ke User jika petugas memiliki akun - final DateTime createdAt; + final String? userId; + final DateTime? createdAt; final DateTime? updatedAt; PetugasDesaModel({ - required this.id, - required this.nama, - this.alamat, + this.id, + this.nama, + this.alamatLengkap, this.noTelp, this.email, this.jabatan, - this.desa, - this.kecamatan, - this.kabupaten, - this.provinsi, this.userId, - required this.createdAt, + this.createdAt, this.updatedAt, }); @@ -40,34 +32,28 @@ class PetugasDesaModel { PetugasDesaModel( id: json["id"], nama: json["nama"], - alamat: json["alamat"], + alamatLengkap: json["alamat_lengkap"], noTelp: json["no_telp"], email: json["email"], jabatan: json["jabatan"], - desa: json["desa"], - kecamatan: json["kecamatan"], - kabupaten: json["kabupaten"], - provinsi: json["provinsi"], userId: json["user_id"], - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, "nama": nama, - "alamat": alamat, + "alamat_lengkap": alamatLengkap, "no_telp": noTelp, "email": email, "jabatan": jabatan, - "desa": desa, - "kecamatan": kecamatan, - "kabupaten": kabupaten, - "provinsi": provinsi, "user_id": userId, - "created_at": createdAt.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/stok_bantuan_model.dart b/lib/app/data/models/stok_bantuan_model.dart index b4d37b3..731493c 100644 --- a/lib/app/data/models/stok_bantuan_model.dart +++ b/lib/app/data/models/stok_bantuan_model.dart @@ -5,7 +5,7 @@ class StokBantuanModel { final String? nama; final String? kategoriBantuanId; final Map? kategoriBantuan; - final double? jumlah; + final double? totalStok; final String? satuan; final String? deskripsi; final DateTime? tanggalMasuk; @@ -18,7 +18,7 @@ class StokBantuanModel { this.nama, this.kategoriBantuanId, this.kategoriBantuan, - this.jumlah, + this.totalStok, this.satuan, this.deskripsi, this.tanggalMasuk, @@ -38,7 +38,8 @@ class StokBantuanModel { nama: json["nama"], kategoriBantuanId: json["kategori_bantuan_id"], kategoriBantuan: json["kategori_bantuan"], - jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, + totalStok: + json["total_stok"] != null ? json["total_stok"].toDouble() : 0.0, satuan: json["satuan"], deskripsi: json["deskripsi"], tanggalMasuk: json["tanggal_masuk"] != null @@ -59,7 +60,7 @@ class StokBantuanModel { final Map data = { "nama": nama, "kategori_bantuan_id": kategoriBantuanId, - "jumlah": jumlah, + "total_stok": totalStok, "satuan": satuan, "deskripsi": deskripsi, "tanggal_masuk": tanggalMasuk?.toIso8601String(), diff --git a/lib/app/data/models/sumber_bantuan_model.dart b/lib/app/data/models/sumber_bantuan_model.dart index 8ae3870..6417b05 100644 --- a/lib/app/data/models/sumber_bantuan_model.dart +++ b/lib/app/data/models/sumber_bantuan_model.dart @@ -1,19 +1,17 @@ import 'dart:convert'; class SumberBantuanModel { - final String id; - final String nama; + final String? id; + final String? nama; final String? deskripsi; - final String? kategori; // Contoh: 'pemerintah', 'swasta', 'masyarakat' - final DateTime createdAt; + final DateTime? createdAt; final DateTime? updatedAt; SumberBantuanModel({ - required this.id, - required this.nama, + this.id, + this.nama, this.deskripsi, - this.kategori, - required this.createdAt, + this.createdAt, this.updatedAt, }); @@ -27,19 +25,19 @@ class SumberBantuanModel { id: json["id"], nama: json["nama"], deskripsi: json["deskripsi"], - kategori: json["kategori"], - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, "nama": nama, "deskripsi": deskripsi, - "kategori": kategori, - "created_at": createdAt.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart index 7c9f4f8..b83e49a 100644 --- a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/donatur_model.dart'; +import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart'; +import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; @@ -9,8 +12,13 @@ import 'package:penyaluran_app/app/services/supabase_service.dart'; class PenitipanBantuanController extends GetxController { final AuthController _authController = Get.find(); final SupabaseService _supabaseService = SupabaseService.to; + final ImagePicker _imagePicker = ImagePicker(); final RxBool isLoading = false.obs; + final RxBool isUploading = false.obs; + + // Path untuk bukti serah terima + final Rx fotoBuktiSerahTerimaPath = Rx(null); // Indeks kategori yang dipilih untuk filter final RxInt selectedCategoryIndex = 0.obs; @@ -22,6 +30,17 @@ class PenitipanBantuanController extends GetxController { final RxInt jumlahTerverifikasi = 0.obs; final RxInt jumlahDitolak = 0.obs; + // Data untuk kategori bantuan + final RxMap stokBantuanMap = + {}.obs; + + // Cache untuk donatur + final RxMap donaturCache = {}.obs; + + // Cache untuk petugas desa + final RxMap> petugasDesaCache = + >{}.obs; + // Controller untuk pencarian final TextEditingController searchController = TextEditingController(); @@ -31,6 +50,11 @@ class PenitipanBantuanController extends GetxController { void onInit() { super.onInit(); loadPenitipanData(); + loadKategoriBantuanData(); + // Tambahkan delay untuk memastikan data petugas desa dimuat setelah penitipan + Future.delayed(const Duration(seconds: 1), () { + loadAllPetugasDesaData(); + }); } @override @@ -56,6 +80,30 @@ class PenitipanBantuanController extends GetxController { .length; jumlahDitolak.value = daftarPenitipan.where((item) => item.status == 'DITOLAK').length; + + // Muat informasi petugas desa untuk item yang terverifikasi + print( + 'Memuat informasi petugas desa untuk ${daftarPenitipan.length} penitipan'); + for (var item in daftarPenitipan) { + if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) { + print( + 'Memuat informasi petugas desa untuk penitipan ID: ${item.id}, petugasDesaId: ${item.petugasDesaId}'); + final petugasData = await getPetugasDesaInfo(item.petugasDesaId); + if (petugasData != null) { + print( + 'Berhasil memuat data petugas desa: ${petugasData['name']} untuk ID: ${item.petugasDesaId}'); + } else { + print( + 'Gagal memuat data petugas desa untuk ID: ${item.petugasDesaId}'); + } + } + } + + // Debug: print semua data petugas desa yang sudah dimuat + print('Data petugas desa yang sudah dimuat:'); + petugasDesaCache.forEach((key, value) { + print('ID: $key, Nama: ${value['name']}'); + }); } } catch (e) { print('Error loading penitipan data: $e'); @@ -64,11 +112,96 @@ class PenitipanBantuanController extends GetxController { } } - Future verifikasiPenitipan(String penitipanId) async { - isLoading.value = true; + Future loadStokBantuanData() async { try { - await _supabaseService.verifikasiPenitipan(penitipanId); + print('Loading stok bantuan data...'); + final stokBantuanData = await _supabaseService.getStokBantuan(); + if (stokBantuanData != null) { + print( + 'Received ${stokBantuanData.length} stok bantuan items from service'); + stokBantuanMap.clear(); // Clear existing data + + for (var data in stokBantuanData) { + final stokBantuan = StokBantuanModel.fromJson(data); + if (stokBantuan.id != null) { + stokBantuanMap[stokBantuan.id!] = stokBantuan; + print( + 'Added stok bantuan: ID=${stokBantuan.id}, Nama=${stokBantuan.nama}, Satuan=${stokBantuan.satuan}'); + } else { + print('Skipped stok bantuan with null ID: $data'); + } + } + print('Loaded ${stokBantuanMap.length} stok bantuan items'); + } else { + print('No stok bantuan data received from service'); + } + } catch (e) { + print('Error loading stok bantuan data: $e'); + } + } + + Future loadKategoriBantuanData() async { + try { + await loadStokBantuanData(); + print( + 'Loaded kategori bantuan data. stokBantuanMap size: ${stokBantuanMap.length}'); + + // Debug: print all stok bantuan items + stokBantuanMap.forEach((key, value) { + print( + 'Stok Bantuan - ID: $key, Nama: ${value.nama}, Satuan: ${value.satuan}'); + }); + } catch (e) { + print('Error loading kategori bantuan data: $e'); + } + } + + Future pickfotoBuktiSerahTerima() async { + try { + final pickedFile = await _imagePicker.pickImage( + source: ImageSource.camera, + imageQuality: 70, + maxWidth: 1000, + ); + + if (pickedFile != null) { + fotoBuktiSerahTerimaPath.value = pickedFile.path; + } + } catch (e) { + print('Error picking image: $e'); + Get.snackbar( + 'Error', + 'Gagal mengambil gambar: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + Future verifikasiPenitipan(String penitipanId) async { + if (fotoBuktiSerahTerimaPath.value == null) { + Get.snackbar( + 'Error', + 'Bukti serah terima harus diupload', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + + isLoading.value = true; + isUploading.value = true; + try { + await _supabaseService.verifikasiPenitipan( + penitipanId, fotoBuktiSerahTerimaPath.value!); + + // Reset path setelah berhasil + fotoBuktiSerahTerimaPath.value = null; + await loadPenitipanData(); + Get.back(); // Tutup dialog Get.snackbar( 'Sukses', 'Penitipan berhasil diverifikasi', @@ -87,6 +220,7 @@ class PenitipanBantuanController extends GetxController { ); } finally { isLoading.value = false; + isUploading.value = false; } } @@ -118,9 +252,17 @@ class PenitipanBantuanController extends GetxController { Future getDonaturInfo(String donaturId) async { try { + // Cek cache terlebih dahulu + if (donaturCache.containsKey(donaturId)) { + return donaturCache[donaturId]; + } + final donaturData = await _supabaseService.getDonaturById(donaturId); if (donaturData != null) { - return DonaturModel.fromJson(donaturData); + final donatur = DonaturModel.fromJson(donaturData); + // Simpan ke cache + donaturCache[donaturId] = donatur; + return donatur; } return null; } catch (e) { @@ -129,10 +271,40 @@ class PenitipanBantuanController extends GetxController { } } + String getKategoriNama(String? stokBantuanId) { + if (stokBantuanId == null) { + print('Stok bantuan ID is null'); + return 'Tidak ada kategori'; + } + + if (!stokBantuanMap.containsKey(stokBantuanId)) { + print('Stok bantuan not found for ID: $stokBantuanId'); + print('Available keys: ${stokBantuanMap.keys.join(', ')}'); + return 'Tidak ada kategori'; + } + + final nama = stokBantuanMap[stokBantuanId]?.nama ?? 'Tidak ada nama'; + print('Found stok bantuan name: $nama for ID: $stokBantuanId'); + return nama; + } + + String getKategoriSatuan(String? stokBantuanId) { + if (stokBantuanId == null) { + return ''; + } + + if (!stokBantuanMap.containsKey(stokBantuanId)) { + return ''; + } + + return stokBantuanMap[stokBantuanId]?.satuan ?? ''; + } + Future refreshData() async { isLoading.value = true; try { await loadPenitipanData(); + await loadKategoriBantuanData(); } finally { isLoading.value = false; } @@ -143,23 +315,135 @@ class PenitipanBantuanController extends GetxController { } List getFilteredPenitipan() { + final searchText = searchController.text.toLowerCase(); + var filteredList = []; + + // Filter berdasarkan status switch (selectedCategoryIndex.value) { case 0: - return daftarPenitipan; + filteredList = daftarPenitipan.toList(); + break; case 1: - return daftarPenitipan - .where((item) => item.status == 'MENUNGGU') - .toList(); + filteredList = + daftarPenitipan.where((item) => item.status == 'MENUNGGU').toList(); + break; case 2: - return daftarPenitipan + filteredList = daftarPenitipan .where((item) => item.status == 'TERVERIFIKASI') .toList(); + break; case 3: - return daftarPenitipan - .where((item) => item.status == 'DITOLAK') - .toList(); + filteredList = + daftarPenitipan.where((item) => item.status == 'DITOLAK').toList(); + break; default: - return daftarPenitipan; + filteredList = daftarPenitipan.toList(); + } + + // Filter berdasarkan pencarian jika ada teks pencarian + if (searchText.isNotEmpty) { + filteredList = filteredList.where((item) { + // Cari berdasarkan deskripsi + final deskripsiMatch = + item.deskripsi?.toLowerCase().contains(searchText) ?? false; + + // Cari berdasarkan kategori + final stokBantuan = getKategoriNama(item.stokBantuanId).toLowerCase(); + final stokBantuanMatch = stokBantuan.contains(searchText); + + return deskripsiMatch || stokBantuanMatch; + }).toList(); + } + + return filteredList; + } + + Future?> getPetugasDesaInfo( + String? petugasDesaId) async { + try { + if (petugasDesaId == null) { + return null; + } + + // Cek cache terlebih dahulu + if (petugasDesaCache.containsKey(petugasDesaId)) { + return petugasDesaCache[petugasDesaId]; + } + + final petugasDesaData = + await _supabaseService.getPetugasDesaById(petugasDesaId); + if (petugasDesaData != null) { + // Simpan ke cache + petugasDesaCache[petugasDesaId] = petugasDesaData; + return petugasDesaData; + } + return null; + } catch (e) { + print('Error getting petugas desa info: $e'); + return null; + } + } + + String getPetugasDesaNama(String? petugasDesaId) { + print('Petugas Desa ID: $petugasDesaId'); + if (petugasDesaId == null) { + return 'Tidak diketahui'; + } + + // Cek apakah data ada di cache + if (!petugasDesaCache.containsKey(petugasDesaId)) { + print( + 'Data petugas desa tidak ditemukan di cache untuk ID: $petugasDesaId'); + // Jadwalkan pengambilan data untuk nanti + loadPetugasDesaData(petugasDesaId); + return 'Memuat...'; + } + + // Sekarang data seharusnya ada di cache + final nama = petugasDesaCache[petugasDesaId]?['name']; + print('Nama petugas desa: $nama untuk ID: $petugasDesaId'); + return nama ?? 'Tidak diketahui'; + } + + // Fungsi untuk memuat data petugas desa dan memperbarui UI + void loadPetugasDesaData(String petugasDesaId) async { + try { + final petugasData = await getPetugasDesaInfo(petugasDesaId); + if (petugasData != null) { + // Data sudah dimasukkan ke cache oleh getPetugasDesaInfo + // Refresh UI + update(); + } else { + print( + 'Gagal mengambil data petugas desa dari server untuk ID: $petugasDesaId'); + } + } catch (e) { + print('Error saat memuat data petugas desa: $e'); + } + } + + // Fungsi untuk memuat semua data petugas desa yang terkait dengan penitipan + void loadAllPetugasDesaData() async { + try { + print('Memuat ulang semua data petugas desa...'); + for (var item in daftarPenitipan) { + if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) { + if (!petugasDesaCache.containsKey(item.petugasDesaId)) { + print('Memuat data petugas desa untuk ID: ${item.petugasDesaId}'); + await getPetugasDesaInfo(item.petugasDesaId); + } + } + } + // Refresh UI + update(); + + // Debug: print semua data petugas desa yang sudah dimuat + print('Data petugas desa yang sudah dimuat setelah reload:'); + petugasDesaCache.forEach((key, value) { + print('ID: $key, Nama: ${value['name']}'); + }); + } catch (e) { + print('Error saat memuat ulang data petugas desa: $e'); } } } diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart index 03c750e..c45bb06 100644 --- a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -57,7 +57,7 @@ class StokBantuanController extends GetxController { // Hitung total stok totalStok.value = 0; for (var item in daftarStokBantuan) { - totalStok.value += item.jumlah ?? 0; + totalStok.value += item.totalStok ?? 0; } // Ambil data stok masuk dan keluar @@ -197,7 +197,9 @@ class StokBantuanController extends GetxController { // Metode untuk mendapatkan jumlah stok yang hampir habis (stok <= 10) int getStokHampirHabis() { - return daftarStokBantuan.where((stok) => (stok.jumlah ?? 0) <= 10).length; + return daftarStokBantuan + .where((stok) => (stok.totalStok ?? 0) <= 10) + .length; } // Metode untuk mendapatkan jumlah stok yang segera kadaluarsa (dalam 30 hari) diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index b314816..52d2d1f 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -1,34 +1,44 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.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/theme/app_theme.dart'; +import 'package:penyaluran_app/app/utils/date_formatter.dart'; +import 'dart:io'; -class PenitipanView extends GetView { +class PenitipanView extends GetView { const PenitipanView({super.key}); - @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Ringkasan penitipan - _buildPenitipanSummary(context), + return Scaffold( + body: Obx(() => RefreshIndicator( + onRefresh: controller.refreshData, + child: controller.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Ringkasan penitipan + _buildPenitipanSummary(context), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Filter dan pencarian - _buildFilterSearch(context), + // Filter dan pencarian + _buildFilterSearch(context), - const SizedBox(height: 20), + const SizedBox(height: 20), - // Daftar penitipan - _buildPenitipanList(context), - ], - ), - ), + // Daftar penitipan + _buildPenitipanList(context), + ], + ), + ), + ), + )), ); } @@ -58,7 +68,8 @@ class PenitipanView extends GetView { context, icon: Icons.pending_actions, title: 'Menunggu', - value: '5', + value: DateFormatter.formatNumber( + controller.jumlahMenunggu.value), color: Colors.orange, ), ), @@ -67,7 +78,8 @@ class PenitipanView extends GetView { context, icon: Icons.check_circle, title: 'Terverifikasi', - value: '12', + value: DateFormatter.formatNumber( + controller.jumlahTerverifikasi.value), color: Colors.green, ), ), @@ -76,7 +88,8 @@ class PenitipanView extends GetView { context, icon: Icons.cancel, title: 'Ditolak', - value: '2', + value: DateFormatter.formatNumber( + controller.jumlahDitolak.value), color: Colors.red, ), ), @@ -133,6 +146,7 @@ class PenitipanView extends GetView { children: [ Expanded( child: TextField( + controller: controller.searchController, decoration: InputDecoration( hintText: 'Cari penitipan...', prefixIcon: const Icon(Icons.search), @@ -144,6 +158,9 @@ class PenitipanView extends GetView { fillColor: Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(vertical: 0), ), + onChanged: (value) { + // Implementasi pencarian + }, ), ), const SizedBox(width: 12), @@ -152,12 +169,30 @@ class PenitipanView extends GetView { color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), - child: IconButton( - onPressed: () { - // Tampilkan dialog filter - }, + child: PopupMenuButton( icon: const Icon(Icons.filter_list), tooltip: 'Filter', + onSelected: (index) { + controller.changeCategory(index); + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 0, + child: Text('Semua'), + ), + const PopupMenuItem( + value: 1, + child: Text('Menunggu'), + ), + const PopupMenuItem( + value: 2, + child: Text('Terverifikasi'), + ), + const PopupMenuItem( + value: 3, + child: Text('Ditolak'), + ), + ], ), ), ], @@ -165,70 +200,78 @@ class PenitipanView extends GetView { } Widget _buildPenitipanList(BuildContext context) { - final List> penitipanList = [ - { - 'id': '1', - 'donatur': 'PT Sejahtera Abadi', - 'kategori_bantuan': 'Sembako', - 'jumlah': '500 kg', - 'tanggal_pengajuan': '15 April 2023', - 'status': 'Menunggu', - }, - { - 'id': '2', - 'donatur': 'Yayasan Peduli Sesama', - 'kategori_bantuan': 'Pakaian', - 'jumlah': '200 pcs', - 'tanggal_pengajuan': '14 April 2023', - 'status': 'Terverifikasi', - }, - { - 'id': '3', - 'donatur': 'Bank BRI', - 'kategori_bantuan': 'Beras', - 'jumlah': '300 kg', - 'tanggal_pengajuan': '13 April 2023', - 'status': 'Terverifikasi', - }, - { - 'id': '4', - 'donatur': 'Komunitas Peduli', - 'kategori_bantuan': 'Alat Tulis', - 'jumlah': '100 set', - 'tanggal_pengajuan': '12 April 2023', - 'status': 'Ditolak', - }, - ]; + final filteredList = controller.getFilteredPenitipan(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Daftar Penitipan', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Daftar Penitipan', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + '${DateFormatter.formatNumber(filteredList.length)} item', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ], ), const SizedBox(height: 12), - ...penitipanList.map((item) => _buildPenitipanItem(context, item)), + filteredList.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + Icon( + Icons.inbox_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Tidak ada data penitipan', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: filteredList.length, + itemBuilder: (context, index) { + return _buildPenitipanItem(context, filteredList[index]); + }, + ), ], ); } - Widget _buildPenitipanItem(BuildContext context, Map item) { + Widget _buildPenitipanItem(BuildContext context, PenitipanBantuanModel item) { Color statusColor; IconData statusIcon; - switch (item['status']) { - case 'Menunggu': + switch (item.status) { + case 'MENUNGGU': statusColor = Colors.orange; statusIcon = Icons.pending_actions; break; - case 'Terverifikasi': + case 'TERVERIFIKASI': statusColor = Colors.green; statusIcon = Icons.check_circle; break; - case 'Ditolak': + case 'DITOLAK': statusColor = Colors.red; statusIcon = Icons.cancel; break; @@ -237,6 +280,20 @@ class PenitipanView extends GetView { statusIcon = Icons.help_outline; } + // Gunakan data donatur dari relasi jika tersedia + final donaturNama = item.donatur?.nama ?? 'Donatur tidak ditemukan'; + + // Debug info + print('PenitipanItem - stokBantuanId: ${item.stokBantuanId}'); + + final kategoriNama = item.kategoriBantuan?.nama ?? + controller.getKategoriNama(item.stokBantuanId); + final kategoriSatuan = item.kategoriBantuan?.satuan ?? + controller.getKategoriSatuan(item.stokBantuanId); + + print( + 'PenitipanItem - kategoriNama: $kategoriNama, kategoriSatuan: $kategoriSatuan'); + return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 12), @@ -262,7 +319,7 @@ class PenitipanView extends GetView { children: [ Expanded( child: Text( - item['donatur'] ?? '', + donaturNama, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -286,7 +343,7 @@ class PenitipanView extends GetView { ), const SizedBox(width: 4), Text( - item['status'] ?? '', + item.status ?? 'Tidak diketahui', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: statusColor, fontWeight: FontWeight.bold, @@ -305,7 +362,7 @@ class PenitipanView extends GetView { context, icon: Icons.category, label: 'Kategori Bantuan', - value: item['kategori_bantuan'] ?? '', + value: kategoriNama, ), ), Expanded( @@ -313,7 +370,8 @@ class PenitipanView extends GetView { context, icon: Icons.inventory, label: 'Jumlah', - value: item['jumlah'] ?? '', + value: + '${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}', ), ), ], @@ -322,17 +380,64 @@ class PenitipanView extends GetView { _buildItemDetail( context, icon: Icons.calendar_today, - label: 'Tanggal Pengajuan', - value: item['tanggal_pengajuan'] ?? '', + label: 'Tanggal Penitipan', + value: DateFormatter.formatDate(item.tanggalPenitipan, + defaultValue: 'Tidak ada tanggal'), ), + + // Tampilkan informasi petugas desa jika status terverifikasi + if (item.status == 'TERVERIFIKASI' && + item.petugasDesaId != null) ...[ + const SizedBox(height: 8), + _buildItemDetail( + context, + icon: Icons.person, + label: 'Diverifikasi Oleh', + value: controller.getPetugasDesaNama(item.petugasDesaId), + ), + ], + + // Tampilkan thumbnail foto bantuan jika ada + if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.photo_library, + size: 16, + color: Colors.grey, + ), + const SizedBox(width: 4), + Text( + 'Foto Bantuan', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(width: 4), + Text( + '(${item.fotoBantuan!.length} foto)', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 12), - if (item['status'] == 'Menunggu') + if (item.status == 'MENUNGGU') Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () { - // Implementasi untuk menerima penitipan + _showVerifikasiDialog(context, item.id ?? ''); }, icon: const Icon(Icons.check, size: 18), label: const Text('Terima'), @@ -343,7 +448,7 @@ class PenitipanView extends GetView { ), TextButton.icon( onPressed: () { - // Implementasi untuk menolak penitipan + _showTolakDialog(context, item.id ?? ''); }, icon: const Icon(Icons.close, size: 18), label: const Text('Tolak'), @@ -354,7 +459,7 @@ class PenitipanView extends GetView { ), TextButton.icon( onPressed: () { - // Implementasi untuk melihat detail penitipan + _showDetailDialog(context, item, donaturNama); }, icon: const Icon(Icons.info_outline, size: 18), label: const Text('Detail'), @@ -371,7 +476,7 @@ class PenitipanView extends GetView { children: [ TextButton.icon( onPressed: () { - // Implementasi untuk melihat detail penitipan + _showDetailDialog(context, item, donaturNama); }, icon: const Icon(Icons.info_outline, size: 18), label: const Text('Detail'), @@ -388,6 +493,399 @@ class PenitipanView extends GetView { ); } + void _showTolakDialog(BuildContext context, String penitipanId) { + final TextEditingController alasanController = TextEditingController(); + + Get.dialog( + AlertDialog( + title: const Text('Tolak Penitipan'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Masukkan alasan penolakan:'), + const SizedBox(height: 16), + TextField( + controller: alasanController, + decoration: const InputDecoration( + hintText: 'Alasan penolakan', + border: OutlineInputBorder(), + ), + maxLines: 3, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () { + if (alasanController.text.trim().isEmpty) { + Get.snackbar( + 'Error', + 'Alasan penolakan tidak boleh kosong', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + return; + } + controller.tolakPenitipan(penitipanId, alasanController.text); + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text('Tolak'), + ), + ], + ), + ); + } + + void _showVerifikasiDialog(BuildContext context, String penitipanId) { + // Reset path bukti serah terima + controller.fotoBuktiSerahTerimaPath.value = null; + + Get.dialog( + AlertDialog( + title: const Text('Verifikasi Penitipan'), + content: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Upload bukti serah terima:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + if (controller.fotoBuktiSerahTerimaPath.value != null) + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(controller.fotoBuktiSerahTerimaPath.value!), + height: 200, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () { + controller.fotoBuktiSerahTerimaPath.value = null; + }, + 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, + ), + ), + ), + ), + ], + ) + else + InkWell( + onTap: controller.pickfotoBuktiSerahTerima, + child: Container( + height: 200, + 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( + 'Ambil Foto', + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Catatan: Foto bukti serah terima wajib diupload untuk verifikasi penitipan.', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Colors.grey, + ), + ), + ], + )), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + Obx(() => ElevatedButton( + onPressed: controller.isUploading.value + ? null + : () => controller.verifikasiPenitipan(penitipanId), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: controller.isUploading.value + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Verifikasi'), + )), + ], + ), + ); + } + + void _showDetailDialog( + BuildContext context, PenitipanBantuanModel item, String donaturNama) { + // Gunakan data kategori dari relasi jika tersedia + final kategoriNama = item.kategoriBantuan?.nama ?? + controller.getKategoriNama(item.stokBantuanId); + final kategoriSatuan = item.kategoriBantuan?.satuan ?? + controller.getKategoriSatuan(item.stokBantuanId); + + Get.dialog( + AlertDialog( + title: const Text('Detail Penitipan'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailItem('Donatur', donaturNama), + _buildDetailItem('Status', item.status ?? 'Tidak diketahui'), + _buildDetailItem('Kategori Bantuan', kategoriNama), + _buildDetailItem('Jumlah', + '${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}'), + _buildDetailItem( + 'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'), + _buildDetailItem( + 'Tanggal Penitipan', + DateFormatter.formatDate(item.tanggalPenitipan, + defaultValue: 'Tidak ada tanggal'), + ), + if (item.tanggalVerifikasi != null) + _buildDetailItem( + 'Tanggal Verifikasi', + DateFormatter.formatDate(item.tanggalVerifikasi), + ), + if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null) + _buildDetailItem( + 'Diverifikasi Oleh', + controller.getPetugasDesaNama(item.petugasDesaId), + ), + if (item.tanggalKadaluarsa != null) + _buildDetailItem( + 'Tanggal Kadaluarsa', + DateFormatter.formatDate(item.tanggalKadaluarsa), + ), + if (item.alasanPenolakan != null && + item.alasanPenolakan!.isNotEmpty) + _buildDetailItem('Alasan Penolakan', item.alasanPenolakan!), + + // Foto Bantuan + if (item.fotoBantuan != null && item.fotoBantuan!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text( + 'Foto Bantuan:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: item.fotoBantuan!.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + _showFullScreenImage( + context, item.fotoBantuan![index]); + }, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + item.fotoBantuan![index], + height: 100, + width: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 100, + width: 100, + color: Colors.grey.shade300, + child: const Icon(Icons.error), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ), + + // Bukti Serah Terima + if (item.fotoBuktiSerahTerima != null && + item.fotoBuktiSerahTerima!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + const Text( + 'Bukti Serah Terima:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () { + _showFullScreenImage( + context, item.fotoBuktiSerahTerima!); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + item.fotoBuktiSerahTerima!, + height: 200, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 200, + width: double.infinity, + color: Colors.grey.shade300, + child: const Icon(Icons.error), + ); + }, + ), + ), + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Tutup'), + ), + ], + ), + ); + } + + void _showFullScreenImage(BuildContext context, String imageUrl) { + Get.dialog( + Dialog( + insetPadding: EdgeInsets.zero, + child: Stack( + fit: StackFit.expand, + children: [ + InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4, + child: Image.network( + imageUrl, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey.shade300, + child: const Center( + child: Icon( + Icons.error, + size: 50, + color: Colors.red, + ), + ), + ); + }, + ), + ), + Positioned( + top: 20, + right: 20, + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildDetailItem(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + Text( + value, + style: const TextStyle(fontSize: 14), + ), + const Divider(), + ], + ), + ); + } + Widget _buildItemDetail( BuildContext context, { required IconData icon, diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 8b501d0..1ea8f72 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -341,9 +341,9 @@ class StokBantuanView extends GetView { child: _buildItemDetail( context, icon: Icons.inventory, - label: 'Jumlah', + label: 'Total Stok', value: - '${DateFormatter.formatNumber(item.jumlah)} ${item.satuan ?? ''}', + '${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}', ), ), Expanded( @@ -452,7 +452,7 @@ class StokBantuanView extends GetView { void _showAddStokDialog(BuildContext context) { final formKey = GlobalKey(); final namaController = TextEditingController(); - final jumlahController = TextEditingController(); + final stokController = TextEditingController(); final satuanController = TextEditingController(); final deskripsiController = TextEditingController(); String? selectedJenisBantuanId; @@ -515,7 +515,7 @@ class StokBantuanView extends GetView { Expanded( flex: 2, child: TextFormField( - controller: jumlahController, + controller: stokController, decoration: const InputDecoration( labelText: 'Jumlah', border: OutlineInputBorder(), @@ -625,7 +625,7 @@ class StokBantuanView extends GetView { if (formKey.currentState!.validate()) { final stok = StokBantuanModel( nama: namaController.text, - jumlah: double.parse(jumlahController.text), + totalStok: double.parse(stokController.text), satuan: satuanController.text, deskripsi: deskripsiController.text, kategoriBantuanId: selectedJenisBantuanId, @@ -649,8 +649,8 @@ class StokBantuanView extends GetView { void _showEditStokDialog(BuildContext context, StokBantuanModel stok) { final formKey = GlobalKey(); final namaController = TextEditingController(text: stok.nama); - final jumlahController = - TextEditingController(text: stok.jumlah?.toString()); + final stokController = + TextEditingController(text: stok.totalStok?.toString()); final satuanController = TextEditingController(text: stok.satuan); final deskripsiController = TextEditingController(text: stok.deskripsi); String? selectedJenisBantuanId = stok.kategoriBantuanId; @@ -718,7 +718,7 @@ class StokBantuanView extends GetView { Expanded( flex: 2, child: TextFormField( - controller: jumlahController, + controller: stokController, decoration: const InputDecoration( labelText: 'Jumlah', border: OutlineInputBorder(), @@ -829,7 +829,7 @@ class StokBantuanView extends GetView { final updatedStok = StokBantuanModel( id: stok.id, nama: namaController.text, - jumlah: double.parse(jumlahController.text), + totalStok: double.parse(stokController.text), satuan: satuanController.text, deskripsi: deskripsiController.text, kategoriBantuanId: selectedJenisBantuanId, diff --git a/lib/app/modules/splash/views/splash_view.dart b/lib/app/modules/splash/views/splash_view.dart index 3071fa5..ee60051 100644 --- a/lib/app/modules/splash/views/splash_view.dart +++ b/lib/app/modules/splash/views/splash_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; -import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class SplashView extends StatefulWidget { diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index 5e92ee6..3336d1c 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'dart:io'; class SupabaseService extends GetxService { static SupabaseService get to => Get.find(); @@ -336,7 +337,7 @@ class SupabaseService extends GetxService { try { final response = await client .from('stok_bantuan') - .select('*, kategori_bantuan:kategori_bantuan_id(id, nama)'); + .select('*, kategori_bantuan:kategori_bantuan_id(*, nama)'); return response; } catch (e) { @@ -429,7 +430,10 @@ class SupabaseService extends GetxService { // Penitipan bantuan methods Future>?> getPenitipanBantuan() async { try { - final response = await client.from('penitipan_bantuan').select('*'); + final response = await client + .from('penitipan_bantuan') + .select('*, donatur:donatur_id(*), stok_bantuan:stok_bantuan_id(*)') + .order('tanggal_penitipan', ascending: false); return response; } catch (e) { @@ -438,13 +442,80 @@ class SupabaseService extends GetxService { } } - Future verifikasiPenitipan(String penitipanId) async { + // Upload file methods + Future uploadFile( + String filePath, String bucket, String folder) async { try { - await client.from('penitipan_bantuan').update({ + final fileName = filePath.split('/').last; + final fileExt = fileName.split('.').last; + final fileKey = + '$folder/${DateTime.now().millisecondsSinceEpoch}.$fileExt'; + + final file = await client.storage.from(bucket).upload( + fileKey, + File(filePath), + fileOptions: const FileOptions(cacheControl: '3600', upsert: true), + ); + + final fileUrl = client.storage.from(bucket).getPublicUrl(fileKey); + print('File uploaded: $fileUrl'); + return fileUrl; + } catch (e) { + print('Error uploading file: $e'); + return null; + } + } + + Future?> uploadMultipleFiles( + List filePaths, String bucket, String folder) async { + try { + final List fileUrls = []; + + for (final filePath in filePaths) { + final fileUrl = await uploadFile(filePath, bucket, folder); + if (fileUrl != null) { + fileUrls.add(fileUrl); + } + } + + return fileUrls; + } catch (e) { + print('Error uploading multiple files: $e'); + return null; + } + } + + Future verifikasiPenitipan( + String penitipanId, String fotoBuktiSerahTerimaPath) async { + try { + // Upload bukti serah terima + final fotoBuktiSerahTerimaUrl = await uploadFile( + fotoBuktiSerahTerimaPath, 'bantuan', 'foto_bukti_serah_terima'); + + if (fotoBuktiSerahTerimaUrl == null) { + throw 'Gagal mengupload bukti serah terima'; + } + + final petugasDesaId = client.auth.currentUser?.id; + print( + 'Verifikasi penitipan dengan ID: $penitipanId oleh petugas desa ID: $petugasDesaId'); + + final updateData = { 'status': 'TERVERIFIKASI', 'tanggal_verifikasi': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(), - }).eq('id', penitipanId); + 'foto_bukti_serah_terima': fotoBuktiSerahTerimaUrl, + 'petugas_desa_id': petugasDesaId, + }; + + print('Data yang akan diupdate: $updateData'); + + await client + .from('penitipan_bantuan') + .update(updateData) + .eq('id', penitipanId); + + print('Penitipan berhasil diverifikasi dan data petugas desa disimpan'); } catch (e) { print('Error verifying penitipan: $e'); throw e.toString(); @@ -744,4 +815,33 @@ class SupabaseService extends GetxService { throw e.toString(); } } + + // Metode untuk mendapatkan informasi petugas desa berdasarkan ID + Future?> getPetugasDesaById(String petugasDesaId) async { + try { + print('Mengambil data petugas desa dengan ID: $petugasDesaId'); + + // Coba ambil dari tabel user_profile dulu + final response = await client + .from('user_profile') + .select('*') + .eq('id', petugasDesaId) + .eq('role', 'PETUGASDESA') + .maybeSingle(); + + print('Response: $response'); + + if (response != null) { + print( + 'Berhasil mendapatkan data petugas desa dari user_profile: $response'); + return response; + } + + print('Data petugas desa tidak ditemukan untuk ID: $petugasDesaId'); + return null; + } catch (e) { + print('Error getting petugas desa by ID: $e'); + return null; + } + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 5a27a5d..ea3bde6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 98d181b..0420466 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux flutter_secure_storage_linux gtk url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ee5d20f..4188def 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import app_links +import file_selector_macos import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation @@ -13,6 +14,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 583c64a..481d68c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -137,6 +145,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" flutter: dependency: "direct main" description: flutter @@ -150,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + url: "https://pub.dev" + source: hosted + version: "2.0.27" flutter_secure_storage: dependency: "direct main" description: @@ -288,6 +336,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9" + url: "https://pub.dev" + source: hosted + version: "0.8.12+22" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 64e2c36..6610285 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,9 @@ dependencies: # Secure storage flutter_secure_storage: ^9.0.0 + # Image picker untuk mengambil gambar dari kamera atau galeri + image_picker: ^1.0.7 + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 563050d..bf6dc58 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a3836b2..b4be188 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + file_selector_windows flutter_secure_storage_windows url_launcher_windows )