Tambahkan dukungan image picker dan izin kamera untuk upload bukti serah terima
- Tambahkan paket image_picker untuk mengambil foto - Perbarui AndroidManifest.xml dan Info.plist untuk izin kamera dan galeri - Tambahkan metode pickfotoBuktiSerahTerima di PenitipanBantuanController - Buat dialog verifikasi dengan fitur upload foto bukti serah terima - Perbarui model PenitipanBantuanModel untuk mendukung foto bukti - Integrasikan upload file ke Supabase storage
This commit is contained in:
@ -1,4 +1,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Izin untuk kamera dan penyimpanan -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="penyaluran_app"
|
android:label="penyaluran_app"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
@ -45,5 +45,11 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Aplikasi memerlukan akses kamera untuk mengambil foto bukti serah terima</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Aplikasi memerlukan akses galeri untuk memilih foto bukti serah terima</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Aplikasi memerlukan akses mikrofon untuk merekam video</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -38,7 +38,7 @@ class DonaturModel {
|
|||||||
email: json["email"],
|
email: json["email"],
|
||||||
jenis: json["jenis"],
|
jenis: json["jenis"],
|
||||||
deskripsi: json["deskripsi"],
|
deskripsi: json["deskripsi"],
|
||||||
status: json["status"],
|
status: json["status"] ?? 'AKTIF',
|
||||||
createdAt: json["created_at"] != null
|
createdAt: json["created_at"] != null
|
||||||
? DateTime.parse(json["created_at"])
|
? DateTime.parse(json["created_at"])
|
||||||
: null,
|
: null,
|
||||||
@ -55,7 +55,7 @@ class DonaturModel {
|
|||||||
"email": email,
|
"email": email,
|
||||||
"jenis": jenis,
|
"jenis": jenis,
|
||||||
"deskripsi": deskripsi,
|
"deskripsi": deskripsi,
|
||||||
"status": status,
|
"status": status ?? 'AKTIF',
|
||||||
"created_at": createdAt?.toIso8601String(),
|
"created_at": createdAt?.toIso8601String(),
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
47
lib/app/data/models/kategori_bantuan_model.dart
Normal file
47
lib/app/data/models/kategori_bantuan_model.dart
Normal file
@ -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<String, dynamic> 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<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"nama": nama,
|
||||||
|
"deskripsi": deskripsi,
|
||||||
|
"satuan": satuan,
|
||||||
|
"created_at": createdAt?.toIso8601String(),
|
||||||
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
@ -1,36 +1,46 @@
|
|||||||
import 'dart:convert';
|
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 {
|
class PenitipanBantuanModel {
|
||||||
final String? id;
|
final String? id;
|
||||||
final String? donaturId;
|
final String? donaturId;
|
||||||
final String? bentukBantuanId;
|
final String? stokBantuanId;
|
||||||
final String? nama;
|
|
||||||
final double? jumlah;
|
final double? jumlah;
|
||||||
final String? satuan;
|
|
||||||
final String? deskripsi;
|
final String? deskripsi;
|
||||||
final String? status;
|
final String? status;
|
||||||
final String? alasanPenolakan;
|
final String? alasanPenolakan;
|
||||||
final List<String>? gambarUrls;
|
final List<String>? fotoBantuan;
|
||||||
final DateTime? tanggalPenitipan;
|
final DateTime? tanggalPenitipan;
|
||||||
final DateTime? tanggalVerifikasi;
|
final DateTime? tanggalVerifikasi;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
final DateTime? tanggalKadaluarsa;
|
||||||
|
final String? petugasDesaId;
|
||||||
|
final String? fotoBuktiSerahTerima;
|
||||||
|
final String? sumberBantuanId;
|
||||||
|
final DonaturModel? donatur;
|
||||||
|
final KategoriBantuanModel? kategoriBantuan;
|
||||||
|
|
||||||
PenitipanBantuanModel({
|
PenitipanBantuanModel({
|
||||||
this.id,
|
this.id,
|
||||||
this.donaturId,
|
this.donaturId,
|
||||||
this.bentukBantuanId,
|
this.stokBantuanId,
|
||||||
this.nama,
|
|
||||||
this.jumlah,
|
this.jumlah,
|
||||||
this.satuan,
|
|
||||||
this.deskripsi,
|
this.deskripsi,
|
||||||
this.status,
|
this.status,
|
||||||
this.alasanPenolakan,
|
this.alasanPenolakan,
|
||||||
this.gambarUrls,
|
this.fotoBantuan,
|
||||||
this.tanggalPenitipan,
|
this.tanggalPenitipan,
|
||||||
this.tanggalVerifikasi,
|
this.tanggalVerifikasi,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
|
this.tanggalKadaluarsa,
|
||||||
|
this.petugasDesaId,
|
||||||
|
this.fotoBuktiSerahTerima,
|
||||||
|
this.sumberBantuanId,
|
||||||
|
this.donatur,
|
||||||
|
this.kategoriBantuan,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PenitipanBantuanModel.fromRawJson(String str) =>
|
factory PenitipanBantuanModel.fromRawJson(String str) =>
|
||||||
@ -42,16 +52,14 @@ class PenitipanBantuanModel {
|
|||||||
PenitipanBantuanModel(
|
PenitipanBantuanModel(
|
||||||
id: json["id"],
|
id: json["id"],
|
||||||
donaturId: json["donatur_id"],
|
donaturId: json["donatur_id"],
|
||||||
bentukBantuanId: json["bentuk_bantuan_id"],
|
stokBantuanId: json["stok_bantuan_id"],
|
||||||
nama: json["nama"],
|
|
||||||
jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0,
|
jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0,
|
||||||
satuan: json["satuan"],
|
|
||||||
deskripsi: json["deskripsi"],
|
deskripsi: json["deskripsi"],
|
||||||
status: json["status"],
|
status: json["status"],
|
||||||
alasanPenolakan: json["alasan_penolakan"],
|
alasanPenolakan: json["alasan_penolakan"],
|
||||||
gambarUrls: json["gambar_urls"] == null
|
fotoBantuan: json["foto_bantuan"] == null
|
||||||
? null
|
? null
|
||||||
: List<String>.from(json["gambar_urls"].map((x) => x)),
|
: List<String>.from(json["foto_bantuan"].map((x) => x)),
|
||||||
tanggalPenitipan: json["tanggal_penitipan"] != null
|
tanggalPenitipan: json["tanggal_penitipan"] != null
|
||||||
? DateTime.parse(json["tanggal_penitipan"])
|
? DateTime.parse(json["tanggal_penitipan"])
|
||||||
: null,
|
: null,
|
||||||
@ -64,24 +72,38 @@ class PenitipanBantuanModel {
|
|||||||
updatedAt: json["updated_at"] != null
|
updatedAt: json["updated_at"] != null
|
||||||
? DateTime.parse(json["updated_at"])
|
? DateTime.parse(json["updated_at"])
|
||||||
: null,
|
: 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<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"donatur_id": donaturId,
|
"donatur_id": donaturId,
|
||||||
"bentuk_bantuan_id": bentukBantuanId,
|
"stok_bantuan_id": stokBantuanId,
|
||||||
"nama": nama,
|
|
||||||
"jumlah": jumlah,
|
"jumlah": jumlah,
|
||||||
"satuan": satuan,
|
|
||||||
"deskripsi": deskripsi,
|
"deskripsi": deskripsi,
|
||||||
"status": status,
|
"status": status,
|
||||||
"alasan_penolakan": alasanPenolakan,
|
"alasan_penolakan": alasanPenolakan,
|
||||||
"gambar_urls": gambarUrls == null
|
"foto_bantuan": fotoBantuan == null
|
||||||
? null
|
? null
|
||||||
: List<dynamic>.from(gambarUrls!.map((x) => x)),
|
: List<dynamic>.from(fotoBantuan!.map((x) => x)),
|
||||||
"tanggal_penitipan": tanggalPenitipan?.toIso8601String(),
|
"tanggal_penitipan": tanggalPenitipan?.toIso8601String(),
|
||||||
"tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(),
|
"tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(),
|
||||||
"created_at": createdAt?.toIso8601String(),
|
"created_at": createdAt?.toIso8601String(),
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
|
"tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(),
|
||||||
|
"petugas_desa_id": petugasDesaId,
|
||||||
|
"foto_bukti_serah_terima": fotoBuktiSerahTerima,
|
||||||
|
"sumber_bantuan_id": sumberBantuanId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,25 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class PetugasDesaModel {
|
class PetugasDesaModel {
|
||||||
final String id;
|
final String? id;
|
||||||
final String nama;
|
final String? nama;
|
||||||
final String? alamat;
|
final String? alamatLengkap;
|
||||||
final String? noTelp;
|
final String? noTelp;
|
||||||
final String? email;
|
final String? email;
|
||||||
final String? jabatan;
|
final String? jabatan;
|
||||||
final String? desa;
|
final String? userId;
|
||||||
final String? kecamatan;
|
final DateTime? createdAt;
|
||||||
final String? kabupaten;
|
|
||||||
final String? provinsi;
|
|
||||||
final String? userId; // Referensi ke User jika petugas memiliki akun
|
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
PetugasDesaModel({
|
PetugasDesaModel({
|
||||||
required this.id,
|
this.id,
|
||||||
required this.nama,
|
this.nama,
|
||||||
this.alamat,
|
this.alamatLengkap,
|
||||||
this.noTelp,
|
this.noTelp,
|
||||||
this.email,
|
this.email,
|
||||||
this.jabatan,
|
this.jabatan,
|
||||||
this.desa,
|
|
||||||
this.kecamatan,
|
|
||||||
this.kabupaten,
|
|
||||||
this.provinsi,
|
|
||||||
this.userId,
|
this.userId,
|
||||||
required this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,34 +32,28 @@ class PetugasDesaModel {
|
|||||||
PetugasDesaModel(
|
PetugasDesaModel(
|
||||||
id: json["id"],
|
id: json["id"],
|
||||||
nama: json["nama"],
|
nama: json["nama"],
|
||||||
alamat: json["alamat"],
|
alamatLengkap: json["alamat_lengkap"],
|
||||||
noTelp: json["no_telp"],
|
noTelp: json["no_telp"],
|
||||||
email: json["email"],
|
email: json["email"],
|
||||||
jabatan: json["jabatan"],
|
jabatan: json["jabatan"],
|
||||||
desa: json["desa"],
|
|
||||||
kecamatan: json["kecamatan"],
|
|
||||||
kabupaten: json["kabupaten"],
|
|
||||||
provinsi: json["provinsi"],
|
|
||||||
userId: json["user_id"],
|
userId: json["user_id"],
|
||||||
createdAt: DateTime.parse(json["created_at"]),
|
createdAt: json["created_at"] != null
|
||||||
updatedAt: json["updated_at"] == null
|
? DateTime.parse(json["created_at"])
|
||||||
? null
|
: null,
|
||||||
: DateTime.parse(json["updated_at"]),
|
updatedAt: json["updated_at"] != null
|
||||||
|
? DateTime.parse(json["updated_at"])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"nama": nama,
|
"nama": nama,
|
||||||
"alamat": alamat,
|
"alamat_lengkap": alamatLengkap,
|
||||||
"no_telp": noTelp,
|
"no_telp": noTelp,
|
||||||
"email": email,
|
"email": email,
|
||||||
"jabatan": jabatan,
|
"jabatan": jabatan,
|
||||||
"desa": desa,
|
|
||||||
"kecamatan": kecamatan,
|
|
||||||
"kabupaten": kabupaten,
|
|
||||||
"provinsi": provinsi,
|
|
||||||
"user_id": userId,
|
"user_id": userId,
|
||||||
"created_at": createdAt.toIso8601String(),
|
"created_at": createdAt?.toIso8601String(),
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ class StokBantuanModel {
|
|||||||
final String? nama;
|
final String? nama;
|
||||||
final String? kategoriBantuanId;
|
final String? kategoriBantuanId;
|
||||||
final Map<String, dynamic>? kategoriBantuan;
|
final Map<String, dynamic>? kategoriBantuan;
|
||||||
final double? jumlah;
|
final double? totalStok;
|
||||||
final String? satuan;
|
final String? satuan;
|
||||||
final String? deskripsi;
|
final String? deskripsi;
|
||||||
final DateTime? tanggalMasuk;
|
final DateTime? tanggalMasuk;
|
||||||
@ -18,7 +18,7 @@ class StokBantuanModel {
|
|||||||
this.nama,
|
this.nama,
|
||||||
this.kategoriBantuanId,
|
this.kategoriBantuanId,
|
||||||
this.kategoriBantuan,
|
this.kategoriBantuan,
|
||||||
this.jumlah,
|
this.totalStok,
|
||||||
this.satuan,
|
this.satuan,
|
||||||
this.deskripsi,
|
this.deskripsi,
|
||||||
this.tanggalMasuk,
|
this.tanggalMasuk,
|
||||||
@ -38,7 +38,8 @@ class StokBantuanModel {
|
|||||||
nama: json["nama"],
|
nama: json["nama"],
|
||||||
kategoriBantuanId: json["kategori_bantuan_id"],
|
kategoriBantuanId: json["kategori_bantuan_id"],
|
||||||
kategoriBantuan: json["kategori_bantuan"],
|
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"],
|
satuan: json["satuan"],
|
||||||
deskripsi: json["deskripsi"],
|
deskripsi: json["deskripsi"],
|
||||||
tanggalMasuk: json["tanggal_masuk"] != null
|
tanggalMasuk: json["tanggal_masuk"] != null
|
||||||
@ -59,7 +60,7 @@ class StokBantuanModel {
|
|||||||
final Map<String, dynamic> data = {
|
final Map<String, dynamic> data = {
|
||||||
"nama": nama,
|
"nama": nama,
|
||||||
"kategori_bantuan_id": kategoriBantuanId,
|
"kategori_bantuan_id": kategoriBantuanId,
|
||||||
"jumlah": jumlah,
|
"total_stok": totalStok,
|
||||||
"satuan": satuan,
|
"satuan": satuan,
|
||||||
"deskripsi": deskripsi,
|
"deskripsi": deskripsi,
|
||||||
"tanggal_masuk": tanggalMasuk?.toIso8601String(),
|
"tanggal_masuk": tanggalMasuk?.toIso8601String(),
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class SumberBantuanModel {
|
class SumberBantuanModel {
|
||||||
final String id;
|
final String? id;
|
||||||
final String nama;
|
final String? nama;
|
||||||
final String? deskripsi;
|
final String? deskripsi;
|
||||||
final String? kategori; // Contoh: 'pemerintah', 'swasta', 'masyarakat'
|
final DateTime? createdAt;
|
||||||
final DateTime createdAt;
|
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
SumberBantuanModel({
|
SumberBantuanModel({
|
||||||
required this.id,
|
this.id,
|
||||||
required this.nama,
|
this.nama,
|
||||||
this.deskripsi,
|
this.deskripsi,
|
||||||
this.kategori,
|
this.createdAt,
|
||||||
required this.createdAt,
|
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,19 +25,19 @@ class SumberBantuanModel {
|
|||||||
id: json["id"],
|
id: json["id"],
|
||||||
nama: json["nama"],
|
nama: json["nama"],
|
||||||
deskripsi: json["deskripsi"],
|
deskripsi: json["deskripsi"],
|
||||||
kategori: json["kategori"],
|
createdAt: json["created_at"] != null
|
||||||
createdAt: DateTime.parse(json["created_at"]),
|
? DateTime.parse(json["created_at"])
|
||||||
updatedAt: json["updated_at"] == null
|
: null,
|
||||||
? null
|
updatedAt: json["updated_at"] != null
|
||||||
: DateTime.parse(json["updated_at"]),
|
? DateTime.parse(json["updated_at"])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"nama": nama,
|
"nama": nama,
|
||||||
"deskripsi": deskripsi,
|
"deskripsi": deskripsi,
|
||||||
"kategori": kategori,
|
"created_at": createdAt?.toIso8601String(),
|
||||||
"created_at": createdAt.toIso8601String(),
|
|
||||||
"updated_at": updatedAt?.toIso8601String(),
|
"updated_at": updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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/penitipan_bantuan_model.dart';
|
||||||
import 'package:penyaluran_app/app/data/models/donatur_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/data/models/user_model.dart';
|
||||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||||
@ -9,8 +12,13 @@ import 'package:penyaluran_app/app/services/supabase_service.dart';
|
|||||||
class PenitipanBantuanController extends GetxController {
|
class PenitipanBantuanController extends GetxController {
|
||||||
final AuthController _authController = Get.find<AuthController>();
|
final AuthController _authController = Get.find<AuthController>();
|
||||||
final SupabaseService _supabaseService = SupabaseService.to;
|
final SupabaseService _supabaseService = SupabaseService.to;
|
||||||
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
|
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxBool isUploading = false.obs;
|
||||||
|
|
||||||
|
// Path untuk bukti serah terima
|
||||||
|
final Rx<String?> fotoBuktiSerahTerimaPath = Rx<String?>(null);
|
||||||
|
|
||||||
// Indeks kategori yang dipilih untuk filter
|
// Indeks kategori yang dipilih untuk filter
|
||||||
final RxInt selectedCategoryIndex = 0.obs;
|
final RxInt selectedCategoryIndex = 0.obs;
|
||||||
@ -22,6 +30,17 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
final RxInt jumlahTerverifikasi = 0.obs;
|
final RxInt jumlahTerverifikasi = 0.obs;
|
||||||
final RxInt jumlahDitolak = 0.obs;
|
final RxInt jumlahDitolak = 0.obs;
|
||||||
|
|
||||||
|
// Data untuk kategori bantuan
|
||||||
|
final RxMap<String, StokBantuanModel> stokBantuanMap =
|
||||||
|
<String, StokBantuanModel>{}.obs;
|
||||||
|
|
||||||
|
// Cache untuk donatur
|
||||||
|
final RxMap<String, DonaturModel> donaturCache = <String, DonaturModel>{}.obs;
|
||||||
|
|
||||||
|
// Cache untuk petugas desa
|
||||||
|
final RxMap<String, Map<String, dynamic>> petugasDesaCache =
|
||||||
|
<String, Map<String, dynamic>>{}.obs;
|
||||||
|
|
||||||
// Controller untuk pencarian
|
// Controller untuk pencarian
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
|
||||||
@ -31,6 +50,11 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
loadPenitipanData();
|
loadPenitipanData();
|
||||||
|
loadKategoriBantuanData();
|
||||||
|
// Tambahkan delay untuk memastikan data petugas desa dimuat setelah penitipan
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
loadAllPetugasDesaData();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -56,6 +80,30 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
.length;
|
.length;
|
||||||
jumlahDitolak.value =
|
jumlahDitolak.value =
|
||||||
daftarPenitipan.where((item) => item.status == 'DITOLAK').length;
|
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) {
|
} catch (e) {
|
||||||
print('Error loading penitipan data: $e');
|
print('Error loading penitipan data: $e');
|
||||||
@ -64,11 +112,96 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifikasiPenitipan(String penitipanId) async {
|
Future<void> loadStokBantuanData() async {
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
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<void> 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<void> 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<void> 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();
|
await loadPenitipanData();
|
||||||
|
Get.back(); // Tutup dialog
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
'Penitipan berhasil diverifikasi',
|
'Penitipan berhasil diverifikasi',
|
||||||
@ -87,6 +220,7 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
isUploading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,9 +252,17 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
|
|
||||||
Future<DonaturModel?> getDonaturInfo(String donaturId) async {
|
Future<DonaturModel?> getDonaturInfo(String donaturId) async {
|
||||||
try {
|
try {
|
||||||
|
// Cek cache terlebih dahulu
|
||||||
|
if (donaturCache.containsKey(donaturId)) {
|
||||||
|
return donaturCache[donaturId];
|
||||||
|
}
|
||||||
|
|
||||||
final donaturData = await _supabaseService.getDonaturById(donaturId);
|
final donaturData = await _supabaseService.getDonaturById(donaturId);
|
||||||
if (donaturData != null) {
|
if (donaturData != null) {
|
||||||
return DonaturModel.fromJson(donaturData);
|
final donatur = DonaturModel.fromJson(donaturData);
|
||||||
|
// Simpan ke cache
|
||||||
|
donaturCache[donaturId] = donatur;
|
||||||
|
return donatur;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} 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<void> refreshData() async {
|
Future<void> refreshData() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await loadPenitipanData();
|
await loadPenitipanData();
|
||||||
|
await loadKategoriBantuanData();
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -143,23 +315,135 @@ class PenitipanBantuanController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<PenitipanBantuanModel> getFilteredPenitipan() {
|
List<PenitipanBantuanModel> getFilteredPenitipan() {
|
||||||
|
final searchText = searchController.text.toLowerCase();
|
||||||
|
var filteredList = <PenitipanBantuanModel>[];
|
||||||
|
|
||||||
|
// Filter berdasarkan status
|
||||||
switch (selectedCategoryIndex.value) {
|
switch (selectedCategoryIndex.value) {
|
||||||
case 0:
|
case 0:
|
||||||
return daftarPenitipan;
|
filteredList = daftarPenitipan.toList();
|
||||||
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
return daftarPenitipan
|
filteredList =
|
||||||
.where((item) => item.status == 'MENUNGGU')
|
daftarPenitipan.where((item) => item.status == 'MENUNGGU').toList();
|
||||||
.toList();
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
return daftarPenitipan
|
filteredList = daftarPenitipan
|
||||||
.where((item) => item.status == 'TERVERIFIKASI')
|
.where((item) => item.status == 'TERVERIFIKASI')
|
||||||
.toList();
|
.toList();
|
||||||
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
return daftarPenitipan
|
filteredList =
|
||||||
.where((item) => item.status == 'DITOLAK')
|
daftarPenitipan.where((item) => item.status == 'DITOLAK').toList();
|
||||||
.toList();
|
break;
|
||||||
default:
|
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<Map<String, dynamic>?> 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class StokBantuanController extends GetxController {
|
|||||||
// Hitung total stok
|
// Hitung total stok
|
||||||
totalStok.value = 0;
|
totalStok.value = 0;
|
||||||
for (var item in daftarStokBantuan) {
|
for (var item in daftarStokBantuan) {
|
||||||
totalStok.value += item.jumlah ?? 0;
|
totalStok.value += item.totalStok ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil data stok masuk dan keluar
|
// Ambil data stok masuk dan keluar
|
||||||
@ -197,7 +197,9 @@ class StokBantuanController extends GetxController {
|
|||||||
|
|
||||||
// Metode untuk mendapatkan jumlah stok yang hampir habis (stok <= 10)
|
// Metode untuk mendapatkan jumlah stok yang hampir habis (stok <= 10)
|
||||||
int getStokHampirHabis() {
|
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)
|
// Metode untuk mendapatkan jumlah stok yang segera kadaluarsa (dalam 30 hari)
|
||||||
|
@ -1,34 +1,44 @@
|
|||||||
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/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/theme/app_theme.dart';
|
||||||
|
import 'package:penyaluran_app/app/utils/date_formatter.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class PenitipanView extends GetView<PetugasDesaController> {
|
class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||||
const PenitipanView({super.key});
|
const PenitipanView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return Scaffold(
|
||||||
child: Padding(
|
body: Obx(() => RefreshIndicator(
|
||||||
padding: const EdgeInsets.all(16.0),
|
onRefresh: controller.refreshData,
|
||||||
child: Column(
|
child: controller.isLoading.value
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? const Center(child: CircularProgressIndicator())
|
||||||
children: [
|
: SingleChildScrollView(
|
||||||
// Ringkasan penitipan
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
_buildPenitipanSummary(context),
|
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
|
// Filter dan pencarian
|
||||||
_buildFilterSearch(context),
|
_buildFilterSearch(context),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Daftar penitipan
|
// Daftar penitipan
|
||||||
_buildPenitipanList(context),
|
_buildPenitipanList(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +68,8 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.pending_actions,
|
icon: Icons.pending_actions,
|
||||||
title: 'Menunggu',
|
title: 'Menunggu',
|
||||||
value: '5',
|
value: DateFormatter.formatNumber(
|
||||||
|
controller.jumlahMenunggu.value),
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -67,7 +78,8 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.check_circle,
|
icon: Icons.check_circle,
|
||||||
title: 'Terverifikasi',
|
title: 'Terverifikasi',
|
||||||
value: '12',
|
value: DateFormatter.formatNumber(
|
||||||
|
controller.jumlahTerverifikasi.value),
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -76,7 +88,8 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.cancel,
|
icon: Icons.cancel,
|
||||||
title: 'Ditolak',
|
title: 'Ditolak',
|
||||||
value: '2',
|
value: DateFormatter.formatNumber(
|
||||||
|
controller.jumlahDitolak.value),
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -133,6 +146,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: controller.searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cari penitipan...',
|
hintText: 'Cari penitipan...',
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
@ -144,6 +158,9 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
fillColor: Colors.grey.shade100,
|
fillColor: Colors.grey.shade100,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||||
),
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
// Implementasi pencarian
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@ -152,12 +169,30 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: PopupMenuButton<int>(
|
||||||
onPressed: () {
|
|
||||||
// Tampilkan dialog filter
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.filter_list),
|
icon: const Icon(Icons.filter_list),
|
||||||
tooltip: 'Filter',
|
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<PetugasDesaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPenitipanList(BuildContext context) {
|
Widget _buildPenitipanList(BuildContext context) {
|
||||||
final List<Map<String, dynamic>> penitipanList = [
|
final filteredList = controller.getFilteredPenitipan();
|
||||||
{
|
|
||||||
'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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
'Daftar Penitipan',
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
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),
|
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<String, dynamic> item) {
|
Widget _buildPenitipanItem(BuildContext context, PenitipanBantuanModel item) {
|
||||||
Color statusColor;
|
Color statusColor;
|
||||||
IconData statusIcon;
|
IconData statusIcon;
|
||||||
|
|
||||||
switch (item['status']) {
|
switch (item.status) {
|
||||||
case 'Menunggu':
|
case 'MENUNGGU':
|
||||||
statusColor = Colors.orange;
|
statusColor = Colors.orange;
|
||||||
statusIcon = Icons.pending_actions;
|
statusIcon = Icons.pending_actions;
|
||||||
break;
|
break;
|
||||||
case 'Terverifikasi':
|
case 'TERVERIFIKASI':
|
||||||
statusColor = Colors.green;
|
statusColor = Colors.green;
|
||||||
statusIcon = Icons.check_circle;
|
statusIcon = Icons.check_circle;
|
||||||
break;
|
break;
|
||||||
case 'Ditolak':
|
case 'DITOLAK':
|
||||||
statusColor = Colors.red;
|
statusColor = Colors.red;
|
||||||
statusIcon = Icons.cancel;
|
statusIcon = Icons.cancel;
|
||||||
break;
|
break;
|
||||||
@ -237,6 +280,20 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
statusIcon = Icons.help_outline;
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@ -262,7 +319,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item['donatur'] ?? '',
|
donaturNama,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@ -286,7 +343,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
item['status'] ?? '',
|
item.status ?? 'Tidak diketahui',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -305,7 +362,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.category,
|
icon: Icons.category,
|
||||||
label: 'Kategori Bantuan',
|
label: 'Kategori Bantuan',
|
||||||
value: item['kategori_bantuan'] ?? '',
|
value: kategoriNama,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -313,7 +370,8 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
context,
|
context,
|
||||||
icon: Icons.inventory,
|
icon: Icons.inventory,
|
||||||
label: 'Jumlah',
|
label: 'Jumlah',
|
||||||
value: item['jumlah'] ?? '',
|
value:
|
||||||
|
'${DateFormatter.formatNumber(item.jumlah)} ${kategoriSatuan}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -322,17 +380,64 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
_buildItemDetail(
|
_buildItemDetail(
|
||||||
context,
|
context,
|
||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
label: 'Tanggal Pengajuan',
|
label: 'Tanggal Penitipan',
|
||||||
value: item['tanggal_pengajuan'] ?? '',
|
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),
|
const SizedBox(height: 12),
|
||||||
if (item['status'] == 'Menunggu')
|
if (item.status == 'MENUNGGU')
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Implementasi untuk menerima penitipan
|
_showVerifikasiDialog(context, item.id ?? '');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check, size: 18),
|
icon: const Icon(Icons.check, size: 18),
|
||||||
label: const Text('Terima'),
|
label: const Text('Terima'),
|
||||||
@ -343,7 +448,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Implementasi untuk menolak penitipan
|
_showTolakDialog(context, item.id ?? '');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close, size: 18),
|
icon: const Icon(Icons.close, size: 18),
|
||||||
label: const Text('Tolak'),
|
label: const Text('Tolak'),
|
||||||
@ -354,7 +459,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Implementasi untuk melihat detail penitipan
|
_showDetailDialog(context, item, donaturNama);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.info_outline, size: 18),
|
icon: const Icon(Icons.info_outline, size: 18),
|
||||||
label: const Text('Detail'),
|
label: const Text('Detail'),
|
||||||
@ -371,7 +476,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Implementasi untuk melihat detail penitipan
|
_showDetailDialog(context, item, donaturNama);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.info_outline, size: 18),
|
icon: const Icon(Icons.info_outline, size: 18),
|
||||||
label: const Text('Detail'),
|
label: const Text('Detail'),
|
||||||
@ -388,6 +493,399 @@ class PenitipanView extends GetView<PetugasDesaController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
Widget _buildItemDetail(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
|
@ -341,9 +341,9 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
child: _buildItemDetail(
|
child: _buildItemDetail(
|
||||||
context,
|
context,
|
||||||
icon: Icons.inventory,
|
icon: Icons.inventory,
|
||||||
label: 'Jumlah',
|
label: 'Total Stok',
|
||||||
value:
|
value:
|
||||||
'${DateFormatter.formatNumber(item.jumlah)} ${item.satuan ?? ''}',
|
'${DateFormatter.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -452,7 +452,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
void _showAddStokDialog(BuildContext context) {
|
void _showAddStokDialog(BuildContext context) {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final namaController = TextEditingController();
|
final namaController = TextEditingController();
|
||||||
final jumlahController = TextEditingController();
|
final stokController = TextEditingController();
|
||||||
final satuanController = TextEditingController();
|
final satuanController = TextEditingController();
|
||||||
final deskripsiController = TextEditingController();
|
final deskripsiController = TextEditingController();
|
||||||
String? selectedJenisBantuanId;
|
String? selectedJenisBantuanId;
|
||||||
@ -515,7 +515,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: jumlahController,
|
controller: stokController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Jumlah',
|
labelText: 'Jumlah',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@ -625,7 +625,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
if (formKey.currentState!.validate()) {
|
if (formKey.currentState!.validate()) {
|
||||||
final stok = StokBantuanModel(
|
final stok = StokBantuanModel(
|
||||||
nama: namaController.text,
|
nama: namaController.text,
|
||||||
jumlah: double.parse(jumlahController.text),
|
totalStok: double.parse(stokController.text),
|
||||||
satuan: satuanController.text,
|
satuan: satuanController.text,
|
||||||
deskripsi: deskripsiController.text,
|
deskripsi: deskripsiController.text,
|
||||||
kategoriBantuanId: selectedJenisBantuanId,
|
kategoriBantuanId: selectedJenisBantuanId,
|
||||||
@ -649,8 +649,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
|
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final namaController = TextEditingController(text: stok.nama);
|
final namaController = TextEditingController(text: stok.nama);
|
||||||
final jumlahController =
|
final stokController =
|
||||||
TextEditingController(text: stok.jumlah?.toString());
|
TextEditingController(text: stok.totalStok?.toString());
|
||||||
final satuanController = TextEditingController(text: stok.satuan);
|
final satuanController = TextEditingController(text: stok.satuan);
|
||||||
final deskripsiController = TextEditingController(text: stok.deskripsi);
|
final deskripsiController = TextEditingController(text: stok.deskripsi);
|
||||||
String? selectedJenisBantuanId = stok.kategoriBantuanId;
|
String? selectedJenisBantuanId = stok.kategoriBantuanId;
|
||||||
@ -718,7 +718,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: jumlahController,
|
controller: stokController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Jumlah',
|
labelText: 'Jumlah',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@ -829,7 +829,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
|
|||||||
final updatedStok = StokBantuanModel(
|
final updatedStok = StokBantuanModel(
|
||||||
id: stok.id,
|
id: stok.id,
|
||||||
nama: namaController.text,
|
nama: namaController.text,
|
||||||
jumlah: double.parse(jumlahController.text),
|
totalStok: double.parse(stokController.text),
|
||||||
satuan: satuanController.text,
|
satuan: satuanController.text,
|
||||||
deskripsi: deskripsiController.text,
|
deskripsi: deskripsiController.text,
|
||||||
kategoriBantuanId: selectedJenisBantuanId,
|
kategoriBantuanId: selectedJenisBantuanId,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/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';
|
|
||||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||||
|
|
||||||
class SplashView extends StatefulWidget {
|
class SplashView extends StatefulWidget {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class SupabaseService extends GetxService {
|
class SupabaseService extends GetxService {
|
||||||
static SupabaseService get to => Get.find<SupabaseService>();
|
static SupabaseService get to => Get.find<SupabaseService>();
|
||||||
@ -336,7 +337,7 @@ class SupabaseService extends GetxService {
|
|||||||
try {
|
try {
|
||||||
final response = await client
|
final response = await client
|
||||||
.from('stok_bantuan')
|
.from('stok_bantuan')
|
||||||
.select('*, kategori_bantuan:kategori_bantuan_id(id, nama)');
|
.select('*, kategori_bantuan:kategori_bantuan_id(*, nama)');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -429,7 +430,10 @@ class SupabaseService extends GetxService {
|
|||||||
// Penitipan bantuan methods
|
// Penitipan bantuan methods
|
||||||
Future<List<Map<String, dynamic>>?> getPenitipanBantuan() async {
|
Future<List<Map<String, dynamic>>?> getPenitipanBantuan() async {
|
||||||
try {
|
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;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -438,13 +442,80 @@ class SupabaseService extends GetxService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifikasiPenitipan(String penitipanId) async {
|
// Upload file methods
|
||||||
|
Future<String?> uploadFile(
|
||||||
|
String filePath, String bucket, String folder) async {
|
||||||
try {
|
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<List<String>?> uploadMultipleFiles(
|
||||||
|
List<String> filePaths, String bucket, String folder) async {
|
||||||
|
try {
|
||||||
|
final List<String> 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<void> 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',
|
'status': 'TERVERIFIKASI',
|
||||||
'tanggal_verifikasi': DateTime.now().toIso8601String(),
|
'tanggal_verifikasi': DateTime.now().toIso8601String(),
|
||||||
'updated_at': 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) {
|
} catch (e) {
|
||||||
print('Error verifying penitipan: $e');
|
print('Error verifying penitipan: $e');
|
||||||
throw e.toString();
|
throw e.toString();
|
||||||
@ -744,4 +815,33 @@ class SupabaseService extends GetxService {
|
|||||||
throw e.toString();
|
throw e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendapatkan informasi petugas desa berdasarkan ID
|
||||||
|
Future<Map<String, dynamic>?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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 =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
gtk
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import app_links
|
import app_links
|
||||||
|
import file_selector_macos
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@ -13,6 +14,7 @@ import url_launcher_macos
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
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"))
|
||||||
|
112
pubspec.lock
112
pubspec.lock
@ -81,6 +81,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
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:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -137,6 +145,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -150,6 +190,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
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:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -288,6 +336,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -61,6 +61,9 @@ dependencies:
|
|||||||
# Secure storage
|
# Secure storage
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
|
|
||||||
|
# Image picker untuk mengambil gambar dari kamera atau galeri
|
||||||
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
@ -7,12 +7,15 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user