ambil data stok bantuan

This commit is contained in:
Khafidh Fuadi
2025-03-11 12:44:32 +07:00
parent d24832ea82
commit eec06ba79d
57 changed files with 4306 additions and 1590 deletions

View File

@ -1,19 +1,21 @@
import 'dart:convert'; import 'dart:convert';
class BentukBantuanModel { class BentukBantuanModel {
final String id; final String? id;
final String nama; final String? nama;
final String? deskripsi; final String? deskripsi;
final String? kategori; final String? kategori;
final DateTime createdAt; final String? satuan;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
BentukBantuanModel({ BentukBantuanModel({
required this.id, this.id,
required this.nama, this.nama,
this.deskripsi, this.deskripsi,
this.kategori, this.kategori,
required this.createdAt, this.satuan,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -28,10 +30,13 @@ class BentukBantuanModel {
nama: json["nama"], nama: json["nama"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
kategori: json["kategori"], kategori: json["kategori"],
createdAt: DateTime.parse(json["created_at"]), satuan: json["satuan"],
updatedAt: json["updated_at"] == null createdAt: json["created_at"] != null
? null ? DateTime.parse(json["created_at"])
: DateTime.parse(json["updated_at"]), : null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -39,7 +44,8 @@ class BentukBantuanModel {
"nama": nama, "nama": nama,
"deskripsi": deskripsi, "deskripsi": deskripsi,
"kategori": kategori, "kategori": kategori,
"created_at": createdAt.toIso8601String(), "satuan": satuan,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -0,0 +1,50 @@
import 'dart:convert';
class DesaModel {
final String id;
final String nama;
final String? kecamatan;
final String? kabupaten;
final String? provinsi;
final DateTime? createdAt;
final DateTime? updatedAt;
DesaModel({
required this.id,
required this.nama,
this.kecamatan,
this.kabupaten,
this.provinsi,
this.createdAt,
this.updatedAt,
});
factory DesaModel.fromRawJson(String str) =>
DesaModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory DesaModel.fromJson(Map<String, dynamic> json) => DesaModel(
id: json["id"],
nama: json["nama"],
kecamatan: json["kecamatan"],
kabupaten: json["kabupaten"],
provinsi: json["provinsi"],
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,
"kecamatan": kecamatan,
"kabupaten": kabupaten,
"provinsi": provinsi,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

View File

@ -1,23 +1,27 @@
import 'dart:convert'; import 'dart:convert';
class DonaturModel { class DonaturModel {
final String id; final String? id;
final String nama; final String? nama;
final String? alamat; final String? alamat;
final String? noTelp; final String? telepon;
final String? email; final String? email;
final String? jenisDonatur; // Individu, Organisasi, Perusahaan, dll final String? jenis;
final DateTime createdAt; final String? deskripsi;
final String? status;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
DonaturModel({ DonaturModel({
required this.id, this.id,
required this.nama, this.nama,
this.alamat, this.alamat,
this.noTelp, this.telepon,
this.email, this.email,
this.jenisDonatur, this.jenis,
required this.createdAt, this.deskripsi,
this.status,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -30,23 +34,29 @@ class DonaturModel {
id: json["id"], id: json["id"],
nama: json["nama"], nama: json["nama"],
alamat: json["alamat"], alamat: json["alamat"],
noTelp: json["no_telp"], telepon: json["telepon"],
email: json["email"], email: json["email"],
jenisDonatur: json["jenis_donatur"], jenis: json["jenis"],
createdAt: DateTime.parse(json["created_at"]), deskripsi: json["deskripsi"],
updatedAt: json["updated_at"] == null status: json["status"],
? null createdAt: json["created_at"] != null
: DateTime.parse(json["updated_at"]), ? DateTime.parse(json["created_at"])
: null,
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": alamat,
"no_telp": noTelp, "telepon": telepon,
"email": email, "email": email,
"jenis_donatur": jenisDonatur, "jenis": jenis,
"created_at": createdAt.toIso8601String(), "deskripsi": deskripsi,
"status": status,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,34 +1,34 @@
import 'dart:convert'; import 'dart:convert';
class LaporanModel { class LaporanModel {
final String id; final String? id;
final String judul; final String? judul;
final String? deskripsi; final String? deskripsi;
final String jenis; // Contoh: 'penyaluran', 'penitipan', 'pengaduan' final String? jenis; // Contoh: 'PENYALURAN', 'STOK_BANTUAN', 'PENERIMA'
final String? final String?
referensiId; // ID dari entitas yang dilaporkan (penyaluran, penitipan, dll) referensiId; // ID dari entitas yang dilaporkan (penyaluran, penitipan, dll)
final String status; // Contoh: 'draft', 'final', 'disetujui' final String? status; // Contoh: 'draft', 'final', 'disetujui'
final String? userId; // Pengguna yang membuat laporan final String? petugasId; // Pengguna yang membuat laporan
final List<String>? fileUrls; // URL file laporan (PDF, Excel, dll) final List<String>? fileUrls; // URL file laporan (PDF, Excel, dll)
final DateTime periodeAwal; final DateTime? tanggalMulai;
final DateTime periodeAkhir; final DateTime? tanggalSelesai;
final DateTime tanggalLaporan; final DateTime? tanggalLaporan;
final DateTime createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
LaporanModel({ LaporanModel({
required this.id, this.id,
required this.judul, this.judul,
this.deskripsi, this.deskripsi,
required this.jenis, this.jenis,
this.referensiId, this.referensiId,
required this.status, this.status,
this.userId, this.petugasId,
this.fileUrls, this.fileUrls,
required this.periodeAwal, this.tanggalMulai,
required this.periodeAkhir, this.tanggalSelesai,
required this.tanggalLaporan, this.tanggalLaporan,
required this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -44,17 +44,29 @@ class LaporanModel {
jenis: json["jenis"], jenis: json["jenis"],
referensiId: json["referensi_id"], referensiId: json["referensi_id"],
status: json["status"], status: json["status"],
userId: json["user_id"], petugasId: json["petugas_id"] ?? json["user_id"],
fileUrls: json["file_urls"] == null fileUrls: json["file_urls"] == null
? null ? null
: List<String>.from(json["file_urls"].map((x) => x)), : List<String>.from(json["file_urls"].map((x) => x)),
periodeAwal: DateTime.parse(json["periode_awal"]), tanggalMulai: json["tanggal_mulai"] != null
periodeAkhir: DateTime.parse(json["periode_akhir"]), ? DateTime.parse(json["tanggal_mulai"])
tanggalLaporan: DateTime.parse(json["tanggal_laporan"]), : json["periode_awal"] != null
createdAt: DateTime.parse(json["created_at"]), ? DateTime.parse(json["periode_awal"])
updatedAt: json["updated_at"] == null : null,
? null tanggalSelesai: json["tanggal_selesai"] != null
: DateTime.parse(json["updated_at"]), ? DateTime.parse(json["tanggal_selesai"])
: json["periode_akhir"] != null
? DateTime.parse(json["periode_akhir"])
: null,
tanggalLaporan: json["tanggal_laporan"] != null
? DateTime.parse(json["tanggal_laporan"])
: null,
createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"])
: null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -64,14 +76,14 @@ class LaporanModel {
"jenis": jenis, "jenis": jenis,
"referensi_id": referensiId, "referensi_id": referensiId,
"status": status, "status": status,
"user_id": userId, "petugas_id": petugasId,
"file_urls": fileUrls == null "file_urls": fileUrls == null
? null ? null
: List<dynamic>.from(fileUrls!.map((x) => x)), : List<dynamic>.from(fileUrls!.map((x) => x)),
"periode_awal": periodeAwal.toIso8601String(), "tanggal_mulai": tanggalMulai?.toIso8601String(),
"periode_akhir": periodeAkhir.toIso8601String(), "tanggal_selesai": tanggalSelesai?.toIso8601String(),
"tanggal_laporan": tanggalLaporan.toIso8601String(), "tanggal_laporan": tanggalLaporan?.toIso8601String(),
"created_at": createdAt.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -46,9 +46,8 @@ class LokasiPenyaluranModel {
kabupaten: json["kabupaten"], kabupaten: json["kabupaten"],
provinsi: json["provinsi"], provinsi: json["provinsi"],
kodePos: json["kode_pos"], kodePos: json["kode_pos"],
latitude: json["latitude"] != null ? json["latitude"].toDouble() : null, latitude: json["latitude"]?.toDouble(),
longitude: longitude: json["longitude"]?.toDouble(),
json["longitude"] != null ? json["longitude"].toDouble() : null,
petugasDesaId: json["petugas_desa_id"], petugasDesaId: json["petugas_desa_id"],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null updatedAt: json["updated_at"] == null

View File

@ -1,25 +1,27 @@
import 'dart:convert'; import 'dart:convert';
class NotifikasiModel { class NotifikasiModel {
final String id; final String? id;
final String judul; final String? userId;
final String pesan; final String? judul;
final String? jenis; // Contoh: 'penyaluran', 'penitipan', 'pengaduan' final String? pesan;
final String? referensiId; // ID dari entitas yang terkait notifikasi final String? jenis;
final String? userId; // Pengguna yang menerima notifikasi final String? referensiId;
final bool dibaca; final bool? dibaca;
final DateTime createdAt; final DateTime? tanggalNotifikasi;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
NotifikasiModel({ NotifikasiModel({
required this.id, this.id,
required this.judul, this.userId,
required this.pesan, this.judul,
this.pesan,
this.jenis, this.jenis,
this.referensiId, this.referensiId,
this.userId, this.dibaca,
this.dibaca = false, this.tanggalNotifikasi,
required this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -31,27 +33,33 @@ class NotifikasiModel {
factory NotifikasiModel.fromJson(Map<String, dynamic> json) => factory NotifikasiModel.fromJson(Map<String, dynamic> json) =>
NotifikasiModel( NotifikasiModel(
id: json["id"], id: json["id"],
userId: json["user_id"],
judul: json["judul"], judul: json["judul"],
pesan: json["pesan"], pesan: json["pesan"],
jenis: json["jenis"], jenis: json["jenis"],
referensiId: json["referensi_id"], referensiId: json["referensi_id"],
userId: json["user_id"], dibaca: json["dibaca"],
dibaca: json["dibaca"] ?? false, tanggalNotifikasi: json["tanggal_notifikasi"] != null
createdAt: DateTime.parse(json["created_at"]), ? DateTime.parse(json["tanggal_notifikasi"])
updatedAt: json["updated_at"] == null : null,
? null createdAt: json["created_at"] != null
: DateTime.parse(json["updated_at"]), ? DateTime.parse(json["created_at"])
: null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"user_id": userId,
"judul": judul, "judul": judul,
"pesan": pesan, "pesan": pesan,
"jenis": jenis, "jenis": jenis,
"referensi_id": referensiId, "referensi_id": referensiId,
"user_id": userId,
"dibaca": dibaca, "dibaca": dibaca,
"created_at": createdAt.toIso8601String(), "tanggal_notifikasi": tanggalNotifikasi?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,25 +1,29 @@
import 'dart:convert'; import 'dart:convert';
class PengaduanModel { class PengaduanModel {
final String id; final String? id;
final String judul; final String? judul;
final String deskripsi; final String? deskripsi;
final String? userId; // Pengguna yang membuat pengaduan final String? status;
final String? penyaluranBantuanId; // Referensi ke PenyaluranBantuan final String? kategori;
final String status; // Contoh: 'pending', 'diproses', 'selesai' final String? pelapor;
final List<String>? gambarUrls; // URL gambar bukti pengaduan final String? kontakPelapor;
final DateTime createdAt; final List<String>? gambarUrls;
final DateTime? tanggalPengaduan;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
PengaduanModel({ PengaduanModel({
required this.id, this.id,
required this.judul, this.judul,
required this.deskripsi, this.deskripsi,
this.userId, this.status,
this.penyaluranBantuanId, this.kategori,
required this.status, this.pelapor,
this.kontakPelapor,
this.gambarUrls, this.gambarUrls,
required this.createdAt, this.tanggalPengaduan,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -32,29 +36,37 @@ class PengaduanModel {
id: json["id"], id: json["id"],
judul: json["judul"], judul: json["judul"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
userId: json["user_id"],
penyaluranBantuanId: json["penyaluran_bantuan_id"],
status: json["status"], status: json["status"],
kategori: json["kategori"],
pelapor: json["pelapor"],
kontakPelapor: json["kontak_pelapor"],
gambarUrls: json["gambar_urls"] == null gambarUrls: json["gambar_urls"] == null
? null ? null
: List<String>.from(json["gambar_urls"].map((x) => x)), : List<String>.from(json["gambar_urls"].map((x) => x)),
createdAt: DateTime.parse(json["created_at"]), tanggalPengaduan: json["tanggal_pengaduan"] != null
updatedAt: json["updated_at"] == null ? DateTime.parse(json["tanggal_pengaduan"])
? 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<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"judul": judul, "judul": judul,
"deskripsi": deskripsi, "deskripsi": deskripsi,
"user_id": userId,
"penyaluran_bantuan_id": penyaluranBantuanId,
"status": status, "status": status,
"kategori": kategori,
"pelapor": pelapor,
"kontak_pelapor": kontakPelapor,
"gambar_urls": gambarUrls == null "gambar_urls": gambarUrls == null
? null ? null
: List<dynamic>.from(gambarUrls!.map((x) => x)), : List<dynamic>.from(gambarUrls!.map((x) => x)),
"created_at": createdAt.toIso8601String(), "tanggal_pengaduan": tanggalPengaduan?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,31 +1,35 @@
import 'dart:convert'; import 'dart:convert';
class PenitipanBantuanModel { class PenitipanBantuanModel {
final String id; final String? id;
final String? donaturId; // Referensi ke Donatur final String? donaturId;
final String? bentukBantuanId; // Referensi ke BentukBantuan final String? bentukBantuanId;
final String? sumberBantuanId; // Referensi ke SumberBantuan final String? nama;
final double jumlah; final double? jumlah;
final String? satuan; // Contoh: kg, buah, paket, dll final String? satuan;
final String? deskripsi; final String? deskripsi;
final String status; // Contoh: 'diterima', 'disalurkan', 'ditolak' final String? status;
final List<String>? gambarUrls; // URL gambar bukti penitipan final String? alasanPenolakan;
final DateTime tanggalPenitipan; final List<String>? gambarUrls;
final DateTime createdAt; final DateTime? tanggalPenitipan;
final DateTime? tanggalVerifikasi;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
PenitipanBantuanModel({ PenitipanBantuanModel({
required this.id, this.id,
this.donaturId, this.donaturId,
this.bentukBantuanId, this.bentukBantuanId,
this.sumberBantuanId, this.nama,
required this.jumlah, this.jumlah,
this.satuan, this.satuan,
this.deskripsi, this.deskripsi,
required this.status, this.status,
this.alasanPenolakan,
this.gambarUrls, this.gambarUrls,
required this.tanggalPenitipan, this.tanggalPenitipan,
required this.createdAt, this.tanggalVerifikasi,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -39,35 +43,45 @@ class PenitipanBantuanModel {
id: json["id"], id: json["id"],
donaturId: json["donatur_id"], donaturId: json["donatur_id"],
bentukBantuanId: json["bentuk_bantuan_id"], bentukBantuanId: json["bentuk_bantuan_id"],
sumberBantuanId: json["sumber_bantuan_id"], nama: json["nama"],
jumlah: json["jumlah"].toDouble(), jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0,
satuan: json["satuan"], satuan: json["satuan"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
status: json["status"], status: json["status"],
alasanPenolakan: json["alasan_penolakan"],
gambarUrls: json["gambar_urls"] == null gambarUrls: json["gambar_urls"] == null
? null ? null
: List<String>.from(json["gambar_urls"].map((x) => x)), : List<String>.from(json["gambar_urls"].map((x) => x)),
tanggalPenitipan: DateTime.parse(json["tanggal_penitipan"]), tanggalPenitipan: json["tanggal_penitipan"] != null
createdAt: DateTime.parse(json["created_at"]), ? DateTime.parse(json["tanggal_penitipan"])
updatedAt: json["updated_at"] == null : null,
? null tanggalVerifikasi: json["tanggal_verifikasi"] != null
: DateTime.parse(json["updated_at"]), ? DateTime.parse(json["tanggal_verifikasi"])
: null,
createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"])
: null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"donatur_id": donaturId, "donatur_id": donaturId,
"bentuk_bantuan_id": bentukBantuanId, "bentuk_bantuan_id": bentukBantuanId,
"sumber_bantuan_id": sumberBantuanId, "nama": nama,
"jumlah": jumlah, "jumlah": jumlah,
"satuan": satuan, "satuan": satuan,
"deskripsi": deskripsi, "deskripsi": deskripsi,
"status": status, "status": status,
"alasan_penolakan": alasanPenolakan,
"gambar_urls": gambarUrls == null "gambar_urls": gambarUrls == null
? null ? null
: List<dynamic>.from(gambarUrls!.map((x) => x)), : List<dynamic>.from(gambarUrls!.map((x) => x)),
"tanggal_penitipan": tanggalPenitipan.toIso8601String(), "tanggal_penitipan": tanggalPenitipan?.toIso8601String(),
"created_at": createdAt.toIso8601String(), "tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,31 +1,29 @@
import 'dart:convert'; import 'dart:convert';
class PenyaluranBantuanModel { class PenyaluranBantuanModel {
final String id; final String? id;
final String? penitipanBantuanId; // Referensi ke PenitipanBantuan final String? judul;
final String? lokasiPenyaluranId; // Referensi ke LokasiPenyaluran
final String? petugasDesaId; // Referensi ke PetugasDesa
final double jumlah;
final String? satuan; // Contoh: kg, buah, paket, dll
final String? deskripsi; final String? deskripsi;
final String status; // Contoh: 'diproses', 'disalurkan', 'dibatalkan' final String? lokasiPenyaluranId;
final List<String>? gambarUrls; // URL gambar bukti penyaluran final String? petugasId;
final DateTime tanggalPenyaluran; final String? status;
final DateTime createdAt; final String? alasanPenolakan;
final DateTime? tanggalPenjadwalan;
final DateTime? tanggalPenyaluran;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
PenyaluranBantuanModel({ PenyaluranBantuanModel({
required this.id, this.id,
this.penitipanBantuanId, this.judul,
this.lokasiPenyaluranId,
this.petugasDesaId,
required this.jumlah,
this.satuan,
this.deskripsi, this.deskripsi,
required this.status, this.lokasiPenyaluranId,
this.gambarUrls, this.petugasId,
required this.tanggalPenyaluran, this.status,
required this.createdAt, this.alasanPenolakan,
this.tanggalPenjadwalan,
this.tanggalPenyaluran,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -37,37 +35,37 @@ class PenyaluranBantuanModel {
factory PenyaluranBantuanModel.fromJson(Map<String, dynamic> json) => factory PenyaluranBantuanModel.fromJson(Map<String, dynamic> json) =>
PenyaluranBantuanModel( PenyaluranBantuanModel(
id: json["id"], id: json["id"],
penitipanBantuanId: json["penitipan_bantuan_id"], judul: json["judul"],
lokasiPenyaluranId: json["lokasi_penyaluran_id"],
petugasDesaId: json["petugas_desa_id"],
jumlah: json["jumlah"].toDouble(),
satuan: json["satuan"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
lokasiPenyaluranId: json["lokasi_penyaluran_id"],
petugasId: json["petugas_id"],
status: json["status"], status: json["status"],
gambarUrls: json["gambar_urls"] == null alasanPenolakan: json["alasan_penolakan"],
? null tanggalPenjadwalan: json["tanggal_penjadwalan"] != null
: List<String>.from(json["gambar_urls"].map((x) => x)), ? DateTime.parse(json["tanggal_penjadwalan"])
tanggalPenyaluran: DateTime.parse(json["tanggal_penyaluran"]), : null,
createdAt: DateTime.parse(json["created_at"]), tanggalPenyaluran: json["tanggal_penyaluran"] != null
updatedAt: json["updated_at"] == null ? DateTime.parse(json["tanggal_penyaluran"])
? 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<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"penitipan_bantuan_id": penitipanBantuanId, "judul": judul,
"lokasi_penyaluran_id": lokasiPenyaluranId,
"petugas_desa_id": petugasDesaId,
"jumlah": jumlah,
"satuan": satuan,
"deskripsi": deskripsi, "deskripsi": deskripsi,
"lokasi_penyaluran_id": lokasiPenyaluranId,
"petugas_id": petugasId,
"status": status, "status": status,
"gambar_urls": gambarUrls == null "alasan_penolakan": alasanPenolakan,
? null "tanggal_penjadwalan": tanggalPenjadwalan?.toIso8601String(),
: List<dynamic>.from(gambarUrls!.map((x) => x)), "tanggal_penyaluran": tanggalPenyaluran?.toIso8601String(),
"tanggal_penyaluran": tanggalPenyaluran.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,27 +1,31 @@
import 'dart:convert'; import 'dart:convert';
class StokBantuanModel { class StokBantuanModel {
final String id; final String? id;
final String bentukBantuanId; // Referensi ke BentukBantuan final String? nama;
final double jumlahMasuk; final String? bentukBantuanId;
final double jumlahKeluar; final String? sumberBantuanId;
final double stokSisa; final double? jumlah;
final String? satuan; final String? satuan;
final String? catatan; final String? deskripsi;
final DateTime tanggalUpdate; final String? status;
final DateTime createdAt; final DateTime? tanggalMasuk;
final DateTime? tanggalKadaluarsa;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
StokBantuanModel({ StokBantuanModel({
required this.id, this.id,
required this.bentukBantuanId, this.nama,
required this.jumlahMasuk, this.bentukBantuanId,
required this.jumlahKeluar, this.sumberBantuanId,
required this.stokSisa, this.jumlah,
this.satuan, this.satuan,
this.catatan, this.deskripsi,
required this.tanggalUpdate, this.status,
required this.createdAt, this.tanggalMasuk,
this.tanggalKadaluarsa,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -33,29 +37,39 @@ class StokBantuanModel {
factory StokBantuanModel.fromJson(Map<String, dynamic> json) => factory StokBantuanModel.fromJson(Map<String, dynamic> json) =>
StokBantuanModel( StokBantuanModel(
id: json["id"], id: json["id"],
nama: json["nama"],
bentukBantuanId: json["bentuk_bantuan_id"], bentukBantuanId: json["bentuk_bantuan_id"],
jumlahMasuk: json["jumlah_masuk"].toDouble(), sumberBantuanId: json["sumber_bantuan_id"],
jumlahKeluar: json["jumlah_keluar"].toDouble(), jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0,
stokSisa: json["stok_sisa"].toDouble(),
satuan: json["satuan"], satuan: json["satuan"],
catatan: json["catatan"], deskripsi: json["deskripsi"],
tanggalUpdate: DateTime.parse(json["tanggal_update"]), status: json["status"],
createdAt: DateTime.parse(json["created_at"]), tanggalMasuk: json["tanggal_masuk"] != null
updatedAt: json["updated_at"] == null ? DateTime.parse(json["tanggal_masuk"])
? null : null,
: DateTime.parse(json["updated_at"]), tanggalKadaluarsa: json["tanggal_kadaluarsa"] != null
? DateTime.parse(json["tanggal_kadaluarsa"])
: null,
createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"])
: null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"nama": nama,
"bentuk_bantuan_id": bentukBantuanId, "bentuk_bantuan_id": bentukBantuanId,
"jumlah_masuk": jumlahMasuk, "sumber_bantuan_id": sumberBantuanId,
"jumlah_keluar": jumlahKeluar, "jumlah": jumlah,
"stok_sisa": stokSisa,
"satuan": satuan, "satuan": satuan,
"catatan": catatan, "deskripsi": deskripsi,
"tanggal_update": tanggalUpdate.toIso8601String(), "status": status,
"created_at": createdAt.toIso8601String(), "tanggal_masuk": tanggalMasuk?.toIso8601String(),
"tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,27 +1,25 @@
import 'dart:convert'; import 'dart:convert';
class TindakanPengaduanModel { class TindakanPengaduanModel {
final String id; final String? id;
final String pengaduanId; // Referensi ke Pengaduan final String? pengaduanId;
final String? userId; // Pengguna yang melakukan tindakan final String? tindakan;
final String tindakan; // Deskripsi tindakan yang dilakukan
final String status; // Contoh: 'diproses', 'selesai'
final String? catatan; final String? catatan;
final List<String>? gambarUrls; // URL gambar bukti tindakan final String? status;
final DateTime tanggalTindakan; final String? petugasId;
final DateTime createdAt; final DateTime? tanggalTindakan;
final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
TindakanPengaduanModel({ TindakanPengaduanModel({
required this.id, this.id,
required this.pengaduanId, this.pengaduanId,
this.userId, this.tindakan,
required this.tindakan,
required this.status,
this.catatan, this.catatan,
this.gambarUrls, this.status,
required this.tanggalTindakan, this.petugasId,
required this.createdAt, this.tanggalTindakan,
this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -34,32 +32,30 @@ class TindakanPengaduanModel {
TindakanPengaduanModel( TindakanPengaduanModel(
id: json["id"], id: json["id"],
pengaduanId: json["pengaduan_id"], pengaduanId: json["pengaduan_id"],
userId: json["user_id"],
tindakan: json["tindakan"], tindakan: json["tindakan"],
status: json["status"],
catatan: json["catatan"], catatan: json["catatan"],
gambarUrls: json["gambar_urls"] == null status: json["status"],
? null petugasId: json["petugas_id"],
: List<String>.from(json["gambar_urls"].map((x) => x)), tanggalTindakan: json["tanggal_tindakan"] != null
tanggalTindakan: DateTime.parse(json["tanggal_tindakan"]), ? DateTime.parse(json["tanggal_tindakan"])
createdAt: DateTime.parse(json["created_at"]), : null,
updatedAt: json["updated_at"] == null createdAt: json["created_at"] != null
? null ? DateTime.parse(json["created_at"])
: DateTime.parse(json["updated_at"]), : null,
updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"])
: null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
"pengaduan_id": pengaduanId, "pengaduan_id": pengaduanId,
"user_id": userId,
"tindakan": tindakan, "tindakan": tindakan,
"status": status,
"catatan": catatan, "catatan": catatan,
"gambar_urls": gambarUrls == null "status": status,
? null "petugas_id": petugasId,
: List<dynamic>.from(gambarUrls!.map((x) => x)), "tanggal_tindakan": tanggalTindakan?.toIso8601String(),
"tanggal_tindakan": tanggalTindakan.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -1,3 +1,5 @@
import 'package:penyaluran_app/app/data/models/desa_model.dart';
class UserModel { class UserModel {
final String id; final String id;
final String email; final String email;
@ -5,6 +7,8 @@ class UserModel {
final String? avatar; final String? avatar;
final String role; final String role;
final bool isActive; final bool isActive;
final DesaModel? desa;
final String? desaId;
final DateTime? lastLogin; final DateTime? lastLogin;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
@ -16,27 +20,41 @@ class UserModel {
this.avatar, this.avatar,
required this.role, required this.role,
this.isActive = true, this.isActive = true,
this.desa,
this.desaId,
this.lastLogin, this.lastLogin,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
factory UserModel.fromJson(Map<String, dynamic> json) { factory UserModel.fromJson(Map<String, dynamic> json) {
if (json['id'] == null || json['email'] == null) {
throw Exception('UserModel: id dan email tidak boleh null');
}
// Parse desa jika ada
DesaModel? desaModel;
if (json['desa'] != null && json['desa'] is Map<String, dynamic>) {
desaModel = DesaModel.fromJson(json['desa'] as Map<String, dynamic>);
}
return UserModel( return UserModel(
id: json['id'], id: json['id'],
email: json['email'], email: json['email'],
name: json['name'], name: json['name'],
avatar: json['avatar'], avatar: json['avatar'],
desa: desaModel,
desaId: json['desa_id'],
role: json['role'] ?? 'WARGA', role: json['role'] ?? 'WARGA',
isActive: json['is_active'] ?? true, isActive: json['is_active'] ?? true,
lastLogin: json['last_login'] != null lastLogin: json['last_login'] != null
? DateTime.parse(json['last_login']) ? DateTime.parse(json['last_login'])
: null, : null,
createdAt: json['CREATED_AT'] != null createdAt: json['created_at'] != null
? DateTime.parse(json['CREATED_AT']) ? DateTime.parse(json['created_at'])
: null, : null,
updatedAt: json['UPDATED_AT'] != null updatedAt: json['updated_at'] != null
? DateTime.parse(json['UPDATED_AT']) ? DateTime.parse(json['updated_at'])
: null, : null,
); );
} }
@ -47,11 +65,13 @@ class UserModel {
'email': email, 'email': email,
'name': name, 'name': name,
'avatar': avatar, 'avatar': avatar,
'desa_id': desaId,
'desa': desa?.toJson(),
'role': role, 'role': role,
'is_active': isActive, 'is_active': isActive,
'last_login': lastLogin?.toIso8601String(), 'last_login': lastLogin?.toIso8601String(),
'CREATED_AT': createdAt?.toIso8601String(), 'created_at': createdAt?.toIso8601String(),
'UPDATED_AT': updatedAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(),
}; };
} }
@ -60,6 +80,8 @@ class UserModel {
String? email, String? email,
String? name, String? name,
String? avatar, String? avatar,
DesaModel? desa,
String? desaId,
String? role, String? role,
bool? isActive, bool? isActive,
DateTime? lastLogin, DateTime? lastLogin,
@ -71,6 +93,8 @@ class UserModel {
email: email ?? this.email, email: email ?? this.email,
name: name ?? this.name, name: name ?? this.name,
avatar: avatar ?? this.avatar, avatar: avatar ?? this.avatar,
desa: desa ?? this.desa,
desaId: desaId ?? this.desaId,
role: role ?? this.role, role: role ?? this.role,
isActive: isActive ?? this.isActive, isActive: isActive ?? this.isActive,
lastLogin: lastLogin ?? this.lastLogin, lastLogin: lastLogin ?? this.lastLogin,
@ -79,3 +103,43 @@ class UserModel {
); );
} }
} }
class User {
final String? id;
final String? name;
final String? email;
final String? phone;
final String? role;
final String? token;
User({
this.id,
this.name,
this.email,
this.phone,
this.role,
this.token,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
phone: json['phone'],
role: json['role'],
token: json['token'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
'role': role,
'token': token,
};
}
}

View File

@ -2,35 +2,39 @@ import 'dart:convert';
// warga == penerima bantuan // warga == penerima bantuan
class WargaModel { class WargaModel {
final String id; final String? id;
final String nama; final String? nama;
final String? nik; final String? nik;
final String? alamat; final String? alamat;
final String? desa; final String? desa;
final String? kecamatan; final String? kecamatan;
final String? kabupaten; final String? kabupaten;
final String? provinsi; final String? provinsi;
final String? noTelp; final String? telepon;
final String? email;
final String? catatan;
final String? kategori; // Contoh: 'lansia', 'disabilitas', 'miskin', dll final String? kategori; // Contoh: 'lansia', 'disabilitas', 'miskin', dll
final String? status; // Contoh: 'aktif', 'nonaktif' final String? status; // Contoh: 'AKTIF', 'NONAKTIF'
final String? lokasiPenyaluranId; // Referensi ke LokasiPenyaluran final String? lokasiPenyaluranId; // Referensi ke LokasiPenyaluran
final DateTime createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
WargaModel({ WargaModel({
required this.id, this.id,
required this.nama, this.nama,
this.nik, this.nik,
this.alamat, this.alamat,
this.desa, this.desa,
this.kecamatan, this.kecamatan,
this.kabupaten, this.kabupaten,
this.provinsi, this.provinsi,
this.noTelp, this.telepon,
this.email,
this.catatan,
this.kategori, this.kategori,
this.status, this.status,
this.lokasiPenyaluranId, this.lokasiPenyaluranId,
required this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
@ -48,14 +52,18 @@ class WargaModel {
kecamatan: json["kecamatan"], kecamatan: json["kecamatan"],
kabupaten: json["kabupaten"], kabupaten: json["kabupaten"],
provinsi: json["provinsi"], provinsi: json["provinsi"],
noTelp: json["no_telp"], telepon: json["telepon"] ?? json["no_telp"],
email: json["email"],
catatan: json["catatan"],
kategori: json["kategori"], kategori: json["kategori"],
status: json["status"], status: json["status"],
lokasiPenyaluranId: json["lokasi_penyaluran_id"], lokasiPenyaluranId: json["lokasi_penyaluran_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() => {
@ -67,11 +75,13 @@ class WargaModel {
"kecamatan": kecamatan, "kecamatan": kecamatan,
"kabupaten": kabupaten, "kabupaten": kabupaten,
"provinsi": provinsi, "provinsi": provinsi,
"no_telp": noTelp, "telepon": telepon,
"email": email,
"catatan": catatan,
"kategori": kategori, "kategori": kategori,
"status": status, "status": status,
"lokasi_penyaluran_id": lokasiPenyaluranId, "lokasi_penyaluran_id": lokasiPenyaluranId,
"created_at": createdAt.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -4,65 +4,43 @@ import 'package:penyaluran_app/app/data/models/user_model.dart';
class AuthProvider { class AuthProvider {
final SupabaseService _supabaseService = SupabaseService.to; final SupabaseService _supabaseService = SupabaseService.to;
// Metode untuk mendaftar pengguna baru // Cache untuk menyimpan data profil pengguna
Future<UserModel?> signUp(String email, String password) async { UserModel? _cachedUser;
try {
final response = await _supabaseService.signUp(email, password);
if (response.user != null) {
// Tunggu beberapa saat agar trigger di database berjalan
await Future.delayed(const Duration(seconds: 1));
// Ambil profil pengguna dari database
final profileData = await _supabaseService.getUserProfile();
if (profileData != null) {
return UserModel.fromJson({
...profileData,
'id': response.user!.id,
'email': response.user!.email!,
});
}
// Jika profil belum tersedia, gunakan data default
return UserModel(
id: response.user!.id,
email: response.user!.email!,
role: 'WARGA', // Default role
);
}
return null;
} catch (e) {
rethrow;
}
}
// Metode untuk login // Metode untuk login
Future<UserModel?> signIn(String email, String password) async { Future<UserModel?> signIn(String email, String password) async {
try { try {
final response = await _supabaseService.signIn(email, password); final response = await _supabaseService.signIn(email, password);
if (response.user != null) { if (response.user != null && response.user?.email != null) {
// Ambil profil pengguna dari database // Ambil profil pengguna dari database
final profileData = await _supabaseService.getUserProfile(); final profileData = await _supabaseService.getUserProfile();
print('DEBUG: Profile data dari signIn: $profileData');
if (profileData != null) { if (profileData != null) {
return UserModel.fromJson({ // Buat UserModel dengan data yang ada
_cachedUser = UserModel.fromJson({
...profileData, ...profileData,
'id': response.user!.id, 'id': response.user!.id,
'email': response.user!.email!, 'email': response.user!.email!,
}); });
print(
'DEBUG: User model dibuat: ${_cachedUser?.name}, desa: ${_cachedUser?.desa?.nama}');
return _cachedUser;
} }
// Jika profil belum tersedia, gunakan data default // Jika profil belum tersedia, gunakan data default
return UserModel( _cachedUser = UserModel(
id: response.user!.id, id: response.user!.id,
email: response.user!.email!, email: response.user!.email!,
role: 'WARGA', // Default role role: 'WARGA', // Default role
); );
print('DEBUG: User model default dibuat: ${_cachedUser?.email}');
return _cachedUser;
} }
return null; return null;
} catch (e) { } catch (e) {
print('Error pada signIn: $e');
rethrow; rethrow;
} }
} }
@ -71,6 +49,7 @@ class AuthProvider {
Future<void> signOut() async { Future<void> signOut() async {
try { try {
await _supabaseService.signOut(); await _supabaseService.signOut();
_cachedUser = null; // Hapus cache saat logout
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@ -78,25 +57,50 @@ class AuthProvider {
// Metode untuk mendapatkan user saat ini // Metode untuk mendapatkan user saat ini
Future<UserModel?> getCurrentUser() async { Future<UserModel?> getCurrentUser() async {
// Jika ada cache dan user masih terautentikasi, gunakan cache
if (_cachedUser != null && _supabaseService.isAuthenticated) {
print(
'DEBUG: Menggunakan data user dari cache: ${_cachedUser?.name}, desa: ${_cachedUser?.desa?.nama}');
return _cachedUser;
}
final user = _supabaseService.currentUser; final user = _supabaseService.currentUser;
if (user != null) { if (user != null) {
try {
// Ambil profil pengguna dari database // Ambil profil pengguna dari database
final profileData = await _supabaseService.getUserProfile(); final profileData = await _supabaseService.getUserProfile();
print('DEBUG: Profile data dari getCurrentUser: $profileData');
if (profileData != null) { if (profileData != null) {
return UserModel.fromJson({ // Buat UserModel dengan data yang ada
_cachedUser = UserModel.fromJson({
...profileData, ...profileData,
'id': user.id, 'id': user.id,
'email': user.email!, 'email': user.email!,
}); });
print(
'DEBUG: User model dibuat: ${_cachedUser?.name}, desa: ${_cachedUser?.desa?.nama}');
return _cachedUser;
} }
// Jika profil belum tersedia, gunakan data default // Jika profil belum tersedia, gunakan data default
return UserModel( _cachedUser = UserModel(
id: user.id, id: user.id,
email: user.email!, email: user.email!,
role: 'WARGA', // Default role role: 'WARGA', // Default role
); );
print('DEBUG: User model default dibuat: ${_cachedUser?.email}');
return _cachedUser;
} catch (e) {
print('Error pada getCurrentUser: $e');
// Jika terjadi error, kembalikan model dengan data minimal
_cachedUser = UserModel(
id: user.id,
email: user.email!,
role: 'WARGA', // Default role
);
return _cachedUser;
}
} }
return null; return null;
} }
@ -132,6 +136,9 @@ class AuthProvider {
tanggalLahir: tanggalLahir, tanggalLahir: tanggalLahir,
agama: agama, agama: agama,
); );
// Invalidasi cache setelah membuat profil baru
_cachedUser = null;
} }
// Metode untuk mendapatkan notifikasi pengguna // Metode untuk mendapatkan notifikasi pengguna

View File

@ -15,6 +15,9 @@ class AuthController extends GetxController {
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxBool isWargaProfileComplete = false.obs; final RxBool isWargaProfileComplete = false.obs;
// Flag untuk menandai apakah sudah melakukan pengambilan data profil
final RxBool _hasLoadedProfile = false.obs;
// Form controllers // Form controllers
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
@ -61,41 +64,76 @@ class AuthController extends GetxController {
// Memeriksa status autentikasi // Memeriksa status autentikasi
Future<void> checkAuthStatus() async { Future<void> checkAuthStatus() async {
if (isLoading.value) {
return; // Hindari pemanggilan berulang jika sedang loading
}
isLoading.value = true; isLoading.value = true;
try { try {
print('Memeriksa status autentikasi...');
// Jika user sudah ada di memori dan profil sudah diambil, gunakan data yang ada
if (_user.value != null && _hasLoadedProfile.value) {
print('Menggunakan data user yang sudah ada di memori');
_handleAuthenticatedUser(_user.value!);
return;
}
// Jika belum ada data user, ambil dari provider
final currentUser = await _authProvider.getCurrentUser(); final currentUser = await _authProvider.getCurrentUser();
if (currentUser != null) { if (currentUser != null) {
print(
'User terautentikasi: ${currentUser.email}, role: ${currentUser.role}');
_user.value = currentUser; _user.value = currentUser;
_hasLoadedProfile.value = true;
// Periksa apakah profil warga sudah lengkap _handleAuthenticatedUser(currentUser);
await checkWargaProfileStatus();
// Hindari navigasi jika sudah berada di halaman yang sesuai
final currentRoute = Get.currentRoute;
// Untuk semua role, arahkan ke dashboard masing-masing
final targetRoute = _getTargetRouteForRole(currentUser.role);
if (currentRoute != targetRoute) {
navigateBasedOnRole(currentUser.role);
}
} else { } else {
// Jika tidak ada user yang login, arahkan ke halaman login print('Tidak ada user yang terautentikasi');
if (Get.currentRoute != Routes.login) { _handleUnauthenticatedUser();
// Bersihkan dependensi form sebelum navigasi
clearFormDependencies();
Get.offAllNamed(Routes.login);
}
} }
} catch (e) { } catch (e) {
print('Error checking auth status: $e'); print('Error checking auth status: $e');
// Jika terjadi error, arahkan ke halaman login print('Stack trace: ${StackTrace.current}');
_handleUnauthenticatedUser();
} finally {
isLoading.value = false;
print('Pemeriksaan status autentikasi selesai');
}
}
// Metode untuk menangani user yang terautentikasi
void _handleAuthenticatedUser(UserModel user) {
// Hindari navigasi jika sudah berada di halaman yang sesuai
final currentRoute = Get.currentRoute;
print('Rute saat ini: $currentRoute');
// Pastikan role tidak null, gunakan default jika null
final role = user.role.isNotEmpty ? user.role : 'WARGA';
print('Role yang digunakan: $role');
// Untuk semua role, arahkan ke dashboard masing-masing
final targetRoute = _getTargetRouteForRole(role);
print('Target rute: $targetRoute');
if (currentRoute != targetRoute) {
print('Navigasi ke rute target berdasarkan role');
navigateBasedOnRole(role);
} else {
print('Sudah berada di rute yang sesuai, tidak perlu navigasi');
}
}
// Metode untuk menangani user yang tidak terautentikasi
void _handleUnauthenticatedUser() {
// Jika tidak ada user yang login, arahkan ke halaman login
if (Get.currentRoute != Routes.login) { if (Get.currentRoute != Routes.login) {
print('Navigasi ke halaman login');
// Bersihkan dependensi form sebelum navigasi // Bersihkan dependensi form sebelum navigasi
clearFormDependencies(); clearFormDependencies();
Get.offAllNamed(Routes.login); Get.offAllNamed(Routes.login);
} } else {
} finally { print('Sudah berada di halaman login');
isLoading.value = false;
} }
} }
@ -154,28 +192,58 @@ class AuthController extends GetxController {
// Metode untuk login // Metode untuk login
Future<void> login() async { Future<void> login() async {
if (!loginFormKey.currentState!.validate()) return; print('DEBUG: Memulai proses login');
if (loginFormKey.currentState == null) {
print('Error: loginFormKey.currentState adalah null');
print('DEBUG: Form key: $loginFormKey');
Get.snackbar(
'Error',
'Terjadi kesalahan pada form login. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
print('DEBUG: Form state ditemukan, melakukan validasi');
if (!loginFormKey.currentState!.validate()) {
print('DEBUG: Validasi form gagal');
return;
}
// Simpan nilai dari controller sebelum melakukan operasi asinkron // Simpan nilai dari controller sebelum melakukan operasi asinkron
final email = emailController.text.trim(); final email = emailController.text.trim();
final password = passwordController.text; final password = passwordController.text;
print('DEBUG: Email: $email, Password length: ${password.length}');
try { try {
print('DEBUG: Mengatur isLoading ke true');
isLoading.value = true; isLoading.value = true;
print('DEBUG: Memanggil _authProvider.signIn');
final user = await _authProvider.signIn( final user = await _authProvider.signIn(
email, email,
password, password,
); );
print('DEBUG: Hasil signIn: ${user != null ? 'Berhasil' : 'Gagal'}');
if (user != null) { if (user != null) {
print('DEBUG: User ditemukan, role: ${user.role}');
_user.value = user; _user.value = user;
_hasLoadedProfile.value = true; // Tandai bahwa profil sudah diambil
clearControllers(); clearControllers();
// Arahkan ke dashboard sesuai peran // Arahkan ke dashboard sesuai peran
print('DEBUG: Navigasi berdasarkan peran: ${user.role}');
navigateBasedOnRole(user.role); navigateBasedOnRole(user.role);
} else {
print('DEBUG: User null setelah login berhasil');
} }
} catch (e) { } catch (e) {
print('Error login: $e'); print('DEBUG: Error detail pada login: $e');
print('DEBUG: Stack trace: ${StackTrace.current}');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Login gagal: ${e.toString()}', 'Login gagal: ${e.toString()}',
@ -184,6 +252,7 @@ class AuthController extends GetxController {
colorText: Colors.white, colorText: Colors.white,
); );
} finally { } finally {
print('DEBUG: Mengatur isLoading ke false');
isLoading.value = false; isLoading.value = false;
} }
} }
@ -193,6 +262,7 @@ class AuthController extends GetxController {
try { try {
await _authProvider.signOut(); await _authProvider.signOut();
_user.value = null; _user.value = null;
_hasLoadedProfile.value = false; // Reset flag saat logout
isWargaProfileComplete.value = false; isWargaProfileComplete.value = false;
// Bersihkan dependensi form sebelum navigasi // Bersihkan dependensi form sebelum navigasi

View File

@ -1,9 +1,15 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
class PenerimaBinding extends Bindings { class PenerimaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Pastikan AuthController tersedia
if (!Get.isRegistered<AuthController>()) {
Get.put(AuthController(), permanent: true);
}
Get.lazyPut<PenerimaController>( Get.lazyPut<PenerimaController>(
() => PenerimaController(), () => PenerimaController(),
fenix: true, fenix: true,

View File

@ -1,16 +1,61 @@
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/modules/petugas_desa/controllers/petugas_desa_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_controller.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/laporan_controller.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
class PetugasDesaBinding extends Bindings { class PetugasDesaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Pastikan AuthController tersedia
if (!Get.isRegistered<AuthController>()) {
Get.put(AuthController(), permanent: true);
}
// Main controller
Get.lazyPut<PetugasDesaController>( Get.lazyPut<PetugasDesaController>(
() => PetugasDesaController(), () => PetugasDesaController(),
fenix: true, fenix: true,
); );
Get.lazyPut<PenerimaController>(
() => PenerimaController(), // Dashboard controller
Get.lazyPut<PetugasDesaDashboardController>(
() => PetugasDesaDashboardController(),
);
// Jadwal penyaluran controller
Get.lazyPut<JadwalPenyaluranController>(
() => JadwalPenyaluranController(),
);
// Stok bantuan controller
Get.lazyPut<StokBantuanController>(
() => StokBantuanController(),
);
// Penitipan bantuan controller
Get.lazyPut<PenitipanBantuanController>(
() => PenitipanBantuanController(),
);
// Pengaduan controller
Get.lazyPut<PengaduanController>(
() => PengaduanController(),
);
// Penerima bantuan controller
Get.lazyPut<PenerimaBantuanController>(
() => PenerimaBantuanController(),
);
// Laporan controller
Get.lazyPut<LaporanController>(
() => LaporanController(),
); );
} }
} }

View File

@ -43,7 +43,9 @@ class GreetingHeader extends StatelessWidget {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
'Kamu Login Sebagai $role${desa != null ? ' $desa' : ''}.', desa != null && desa!.isNotEmpty
? 'Kamu Login Sebagai $role $desa.'
: 'Kamu Login Sebagai $role.',
style: textTheme.bodyMedium?.copyWith( style: textTheme.bodyMedium?.copyWith(
fontSize: 14, fontSize: 14,
color: Colors.grey[600], color: Colors.grey[600],

View File

@ -1,21 +1,21 @@
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/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart';
class JadwalSectionWidget extends StatelessWidget { class JadwalSectionWidget extends StatelessWidget {
final PetugasDesaController controller; final JadwalPenyaluranController controller;
final String title; final String title;
final List<Map<String, dynamic>> jadwalList; final List<dynamic> jadwalList;
final String status; final String status;
const JadwalSectionWidget({ const JadwalSectionWidget({
Key? key, super.key,
required this.controller, required this.controller,
required this.title, required this.title,
required this.jadwalList, required this.jadwalList,
required this.status, required this.status,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -62,20 +62,24 @@ class JadwalSectionWidget extends StatelessWidget {
); );
} }
List<Map<String, dynamic>> _getCurrentJadwalList() { List<dynamic> _getCurrentJadwalList() {
switch (title) { switch (title) {
case 'Hari Ini': case 'Hari Ini':
return controller.jadwalHariIni; return controller.jadwalHariIni.toList();
case 'Mendatang': case 'Mendatang':
return controller.jadwalMendatang; return controller.jadwalMendatang.toList();
case 'Selesai': case 'Selesai':
return controller.jadwalSelesai; return controller.jadwalSelesai.toList();
default: default:
return jadwalList; return jadwalList;
} }
} }
Widget _buildJadwalItem(TextTheme textTheme, Map<String, dynamic> jadwal) { Widget _buildJadwalItem(TextTheme textTheme, dynamic jadwal) {
// Konversi jadwal ke Map jika itu adalah PenyaluranBantuanModel
final Map<String, dynamic> jadwalData =
jadwal is Map<String, dynamic> ? jadwal : jadwal.toJson();
Color statusColor; Color statusColor;
switch (status) { switch (status) {
case 'Aktif': case 'Aktif':
@ -94,7 +98,7 @@ class JadwalSectionWidget extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
// Navigasi ke halaman pelaksanaan penyaluran dengan data jadwal // Navigasi ke halaman pelaksanaan penyaluran dengan data jadwal
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwal); Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwalData);
}, },
child: Container( child: Container(
width: double.infinity, width: double.infinity,
@ -120,7 +124,7 @@ class JadwalSectionWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
jadwal['lokasi'] ?? '', jadwalData['lokasi'] ?? '',
style: textTheme.titleMedium?.copyWith( style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -144,23 +148,23 @@ class JadwalSectionWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Jenis Bantuan: ${jadwal['jenis_bantuan'] ?? ''}', 'Jenis Bantuan: ${jadwalData['jenis_bantuan'] ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Tanggal: ${jadwal['tanggal'] ?? ''}', 'Tanggal: ${jadwalData['tanggal'] ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Waktu: ${jadwal['waktu'] ?? ''}', 'Waktu: ${jadwalData['waktu'] ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
if (jadwal['jumlah_penerima'] != null) ...[ if (jadwalData['jumlah_penerima'] != null) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Jumlah Penerima: ${jadwal['jumlah_penerima']}', 'Jumlah Penerima: ${jadwalData['jumlah_penerima']}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
], ],

View File

@ -1,16 +1,16 @@
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/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.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 PermintaanPenjadwalanSummaryWidget extends StatelessWidget { class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
final PetugasDesaController controller; final JadwalPenyaluranController controller;
const PermintaanPenjadwalanSummaryWidget({ const PermintaanPenjadwalanSummaryWidget({
Key? key, super.key,
required this.controller, required this.controller,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -134,8 +134,11 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
}); });
} }
Widget _buildPermintaanPreview( Widget _buildPermintaanPreview(TextTheme textTheme, dynamic permintaan) {
TextTheme textTheme, Map<String, dynamic> permintaan) { // Konversi permintaan ke Map jika itu adalah PenyaluranBantuanModel
final Map<String, dynamic> permintaanData =
permintaan is Map<String, dynamic> ? permintaan : permintaan.toJson();
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@ -152,7 +155,7 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
permintaan['nama'] ?? '', permintaanData['nama'] ?? '',
style: textTheme.titleSmall?.copyWith( style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -178,12 +181,12 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Jenis: ${permintaan['jenis_bantuan'] ?? ''}', 'Jenis: ${permintaanData['jenis_bantuan'] ?? ''}',
style: textTheme.bodySmall, style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(
'Tanggal: ${permintaan['tanggal_permintaan'] ?? ''}', 'Tanggal: ${permintaanData['tanggal_permintaan'] ?? ''}',
style: textTheme.bodySmall, style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@ -1,15 +1,16 @@
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/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanWidget extends StatelessWidget { class PermintaanPenjadwalanWidget extends StatelessWidget {
final PetugasDesaController controller; final JadwalPenyaluranController controller;
const PermintaanPenjadwalanWidget({ const PermintaanPenjadwalanWidget({
Key? key, super.key,
required this.controller, required this.controller,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -90,7 +91,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
// Widget untuk menampilkan item permintaan penjadwalan // Widget untuk menampilkan item permintaan penjadwalan
Widget _buildPermintaanItem( Widget _buildPermintaanItem(
TextTheme textTheme, Map<String, dynamic> permintaan) { TextTheme textTheme, PenyaluranBantuanModel permintaan) {
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
@ -119,7 +120,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
permintaan['nama'] ?? '', permintaan.judul ?? '',
style: textTheme.titleMedium?.copyWith( style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -143,22 +144,22 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'NIK: ${permintaan['nik'] ?? ''}', 'ID: ${permintaan.id ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Jenis Bantuan: ${permintaan['jenis_bantuan'] ?? ''}', 'Jenis Bantuan: ${permintaan.judul ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Tanggal Permintaan: ${permintaan['tanggal_permintaan'] ?? ''}', 'Tanggal Permintaan: ${permintaan.createdAt?.toString().substring(0, 10) ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Alamat: ${permintaan['alamat'] ?? ''}', 'Deskripsi: ${permintaan.deskripsi ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -191,15 +192,15 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
} }
// Dialog untuk konfirmasi permintaan // Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) { void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) {
String? selectedJadwalId; String? selectedJadwalId;
// Data jadwal yang tersedia dari controller // Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) { final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: jadwal['id'], value: jadwal.id,
child: Text( child: Text(
'${jadwal['tanggal']} - ${jadwal['lokasi']} (${jadwal['jenis_bantuan']})'), '${jadwal.tanggalPenjadwalan?.toString().substring(0, 10) ?? ''} - ${jadwal.lokasiPenyaluranId ?? ''} (${jadwal.judul ?? ''})'),
); );
}).toList(); }).toList();
@ -219,7 +220,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama']}.'), 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan.judul}.'),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('Pilih jadwal penyaluran:'), const Text('Pilih jadwal penyaluran:'),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -245,9 +246,8 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
onPressed: () { onPressed: () {
if (selectedJadwalId != null) { if (selectedJadwalId != null) {
// Panggil metode konfirmasi di controller // Panggil metode konfirmasi di controller
controller.konfirmasiPermintaanPenjadwalan( controller.approveJadwal(
permintaan['id'], permintaan.id ?? '',
selectedJadwalId!,
); );
Get.back(); Get.back();
@ -279,7 +279,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
} }
// Dialog untuk menolak permintaan // Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) { void _showTolakDialog(PenyaluranBantuanModel permintaan) {
final TextEditingController alasanController = TextEditingController(); final TextEditingController alasanController = TextEditingController();
Get.dialog( Get.dialog(
@ -290,7 +290,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama']}.'), 'Anda akan menolak permintaan penjadwalan dari ${permintaan.judul}.'),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('Alasan penolakan:'), const Text('Alasan penolakan:'),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -313,8 +313,8 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
onPressed: () { onPressed: () {
if (alasanController.text.trim().isNotEmpty) { if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller // Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan( controller.rejectJadwal(
permintaan['id'], permintaan.id ?? '',
alasanController.text.trim(), alasanController.text.trim(),
); );

View File

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_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';
class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk jadwal
final RxList<PenyaluranBantuanModel> jadwalHariIni =
<PenyaluranBantuanModel>[].obs;
final RxList<PenyaluranBantuanModel> jadwalMendatang =
<PenyaluranBantuanModel>[].obs;
final RxList<PenyaluranBantuanModel> jadwalSelesai =
<PenyaluranBantuanModel>[].obs;
// Data untuk permintaan penjadwalan
final RxList<PenyaluranBantuanModel> permintaanPenjadwalan =
<PenyaluranBantuanModel>[].obs;
final RxInt jumlahPermintaanPenjadwalan = 0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadJadwalData();
loadPermintaanPenjadwalanData();
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
Future<void> loadJadwalData() async {
isLoading.value = true;
try {
// Mengambil data jadwal hari ini
final jadwalHariIniData = await _supabaseService.getJadwalHariIni();
if (jadwalHariIniData != null) {
jadwalHariIni.value = jadwalHariIniData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
// Mengambil data jadwal mendatang
final jadwalMendatangData = await _supabaseService.getJadwalMendatang();
if (jadwalMendatangData != null) {
jadwalMendatang.value = jadwalMendatangData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
// Mengambil data jadwal selesai
final jadwalSelesaiData = await _supabaseService.getJadwalSelesai();
if (jadwalSelesaiData != null) {
jadwalSelesai.value = jadwalSelesaiData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
} catch (e) {
print('Error loading jadwal data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> loadPermintaanPenjadwalanData() async {
try {
final permintaanData = await _supabaseService.getPermintaanPenjadwalan();
if (permintaanData != null) {
permintaanPenjadwalan.value = permintaanData
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
}
} catch (e) {
print('Error loading permintaan penjadwalan data: $e');
}
}
Future<void> approveJadwal(String jadwalId) async {
isLoading.value = true;
try {
await _supabaseService.approveJadwal(jadwalId);
await loadPermintaanPenjadwalanData();
await loadJadwalData();
Get.snackbar(
'Sukses',
'Jadwal berhasil disetujui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error approving jadwal: $e');
Get.snackbar(
'Error',
'Gagal menyetujui jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> rejectJadwal(String jadwalId, String alasan) async {
isLoading.value = true;
try {
await _supabaseService.rejectJadwal(jadwalId, alasan);
await loadPermintaanPenjadwalanData();
Get.snackbar(
'Sukses',
'Jadwal berhasil ditolak',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error rejecting jadwal: $e');
Get.snackbar(
'Error',
'Gagal menolak jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> completeJadwal(String jadwalId) async {
isLoading.value = true;
try {
await _supabaseService.completeJadwal(jadwalId);
await loadJadwalData();
Get.snackbar(
'Sukses',
'Jadwal berhasil diselesaikan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error completing jadwal: $e');
Get.snackbar(
'Error',
'Gagal menyelesaikan jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadJadwalData();
await loadPermintaanPenjadwalanData();
} finally {
isLoading.value = false;
}
}
void changeCategory(int index) {
selectedCategoryIndex.value = index;
}
}

View File

@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/laporan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class LaporanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk laporan
final RxList<LaporanModel> daftarLaporan = <LaporanModel>[].obs;
// Filter tanggal
final Rx<DateTime?> tanggalMulai = Rx<DateTime?>(null);
final Rx<DateTime?> tanggalSelesai = Rx<DateTime?>(null);
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
// Set default tanggal filter (1 bulan terakhir)
tanggalSelesai.value = DateTime.now();
tanggalMulai.value = DateTime.now().subtract(const Duration(days: 30));
loadLaporanData();
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
Future<void> loadLaporanData() async {
isLoading.value = true;
try {
final laporanData = await _supabaseService.getLaporan(
tanggalMulai.value,
tanggalSelesai.value,
);
if (laporanData != null) {
daftarLaporan.value =
laporanData.map((data) => LaporanModel.fromJson(data)).toList();
}
} catch (e) {
print('Error loading laporan data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> generateLaporan(String jenis) async {
isLoading.value = true;
try {
final laporan = LaporanModel(
jenis: jenis,
tanggalMulai: tanggalMulai.value,
tanggalSelesai: tanggalSelesai.value,
petugasId: user?.id,
createdAt: DateTime.now(),
);
final laporanId =
await _supabaseService.generateLaporan(laporan.toJson());
if (laporanId != null) {
await loadLaporanData();
Get.snackbar(
'Sukses',
'Laporan berhasil dibuat',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
} catch (e) {
print('Error generating laporan: $e');
Get.snackbar(
'Error',
'Gagal membuat laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> downloadLaporan(String laporanId) async {
isLoading.value = true;
try {
final url = await _supabaseService.downloadLaporan(laporanId);
if (url != null) {
// Implementasi download file
Get.snackbar(
'Sukses',
'Laporan berhasil diunduh',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
} catch (e) {
print('Error downloading laporan: $e');
Get.snackbar(
'Error',
'Gagal mengunduh laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> deleteLaporan(String laporanId) async {
isLoading.value = true;
try {
await _supabaseService.deleteLaporan(laporanId);
await loadLaporanData();
Get.snackbar(
'Sukses',
'Laporan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error deleting laporan: $e');
Get.snackbar(
'Error',
'Gagal menghapus laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
void setTanggalMulai(DateTime tanggal) {
tanggalMulai.value = tanggal;
}
void setTanggalSelesai(DateTime tanggal) {
tanggalSelesai.value = tanggal;
}
Future<void> applyFilter() async {
await loadLaporanData();
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadLaporanData();
} finally {
isLoading.value = false;
}
}
void changeCategory(int index) {
selectedCategoryIndex.value = index;
}
List<LaporanModel> getFilteredLaporan() {
switch (selectedCategoryIndex.value) {
case 0:
return daftarLaporan;
case 1:
return daftarLaporan
.where((item) => item.jenis == 'PENYALURAN')
.toList();
case 2:
return daftarLaporan
.where((item) => item.jenis == 'STOK_BANTUAN')
.toList();
case 3:
return daftarLaporan.where((item) => item.jenis == 'PENERIMA').toList();
default:
return daftarLaporan;
}
}
}

View File

@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/warga_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class PenerimaBantuanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk penerima bantuan
final RxList<WargaModel> daftarPenerima = <WargaModel>[].obs;
final RxInt totalPenerima = 0.obs;
final RxInt totalPenerimaAktif = 0.obs;
final RxInt totalPenerimaNonaktif = 0.obs;
// Controller untuk pencarian dan form
final TextEditingController searchController = TextEditingController();
final TextEditingController namaController = TextEditingController();
final TextEditingController nikController = TextEditingController();
final TextEditingController alamatController = TextEditingController();
final TextEditingController teleponController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController catatanController = TextEditingController();
// Form key
final GlobalKey<FormState> penerimaFormKey = GlobalKey<FormState>();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadPenerimaData();
}
@override
void onClose() {
searchController.dispose();
namaController.dispose();
nikController.dispose();
alamatController.dispose();
teleponController.dispose();
emailController.dispose();
catatanController.dispose();
super.onClose();
}
Future<void> loadPenerimaData() async {
isLoading.value = true;
try {
final penerimaData = await _supabaseService.getPenerimaBantuan();
if (penerimaData != null) {
daftarPenerima.value =
penerimaData.map((data) => WargaModel.fromJson(data)).toList();
// Hitung total
totalPenerima.value = daftarPenerima.length;
totalPenerimaAktif.value =
daftarPenerima.where((item) => item.status == 'AKTIF').length;
totalPenerimaNonaktif.value =
daftarPenerima.where((item) => item.status == 'NONAKTIF').length;
}
} catch (e) {
print('Error loading penerima data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> tambahPenerima() async {
if (!penerimaFormKey.currentState!.validate()) return;
isLoading.value = true;
try {
final penerima = WargaModel(
nama: namaController.text,
nik: nikController.text,
alamat: alamatController.text,
telepon: teleponController.text,
email: emailController.text,
catatan: catatanController.text,
status: 'AKTIF',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await _supabaseService.tambahPenerima(penerima.toJson());
// Clear form
clearForm();
await loadPenerimaData();
Get.back(); // Close dialog
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error adding penerima: $e');
Get.snackbar(
'Error',
'Gagal menambahkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> updatePenerima(String penerimaId) async {
if (!penerimaFormKey.currentState!.validate()) return;
isLoading.value = true;
try {
final penerima = WargaModel(
id: penerimaId,
nama: namaController.text,
nik: nikController.text,
alamat: alamatController.text,
telepon: teleponController.text,
email: emailController.text,
catatan: catatanController.text,
updatedAt: DateTime.now(),
);
await _supabaseService.updatePenerima(penerimaId, penerima.toJson());
// Clear form
clearForm();
await loadPenerimaData();
Get.back(); // Close dialog
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error updating penerima: $e');
Get.snackbar(
'Error',
'Gagal memperbarui penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> nonaktifkanPenerima(String penerimaId) async {
isLoading.value = true;
try {
await _supabaseService.updateStatusPenerima(penerimaId, 'NONAKTIF');
await loadPenerimaData();
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil dinonaktifkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error deactivating penerima: $e');
Get.snackbar(
'Error',
'Gagal menonaktifkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> aktifkanPenerima(String penerimaId) async {
isLoading.value = true;
try {
await _supabaseService.updateStatusPenerima(penerimaId, 'AKTIF');
await loadPenerimaData();
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil diaktifkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error activating penerima: $e');
Get.snackbar(
'Error',
'Gagal mengaktifkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
void setFormData(WargaModel penerima) {
namaController.text = penerima.nama ?? '';
nikController.text = penerima.nik ?? '';
alamatController.text = penerima.alamat ?? '';
teleponController.text = penerima.telepon ?? '';
emailController.text = penerima.email ?? '';
catatanController.text = penerima.catatan ?? '';
}
void clearForm() {
namaController.clear();
nikController.clear();
alamatController.clear();
teleponController.clear();
emailController.clear();
catatanController.clear();
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadPenerimaData();
} finally {
isLoading.value = false;
}
}
void changeCategory(int index) {
selectedCategoryIndex.value = index;
}
List<WargaModel> getFilteredPenerima() {
switch (selectedCategoryIndex.value) {
case 0:
return daftarPenerima;
case 1:
return daftarPenerima.where((item) => item.status == 'AKTIF').toList();
case 2:
return daftarPenerima
.where((item) => item.status == 'NONAKTIF')
.toList();
default:
return daftarPenerima;
}
}
// Validasi form
String? validateNama(String? value) {
if (value == null || value.isEmpty) {
return 'Nama tidak boleh kosong';
}
return null;
}
String? validateNIK(String? value) {
if (value == null || value.isEmpty) {
return 'NIK tidak boleh kosong';
}
if (value.length != 16) {
return 'NIK harus 16 digit';
}
return null;
}
String? validateAlamat(String? value) {
if (value == null || value.isEmpty) {
return 'Alamat tidak boleh kosong';
}
return null;
}
String? validateTelepon(String? value) {
if (value == null || value.isEmpty) {
return 'Nomor telepon tidak boleh kosong';
}
return null;
}
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return null; // Email boleh kosong
}
if (!GetUtils.isEmail(value)) {
return 'Email tidak valid';
}
return null;
}
}

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class PenerimaController extends GetxController { class PenerimaController extends GetxController {
final RxList<Map<String, dynamic>> daftarPenerima = final RxList<Map<String, dynamic>> daftarPenerima =

View File

@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class PengaduanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk pengaduan
final RxList<PengaduanModel> daftarPengaduan = <PengaduanModel>[].obs;
final RxInt jumlahDiproses = 0.obs;
final RxInt jumlahTindakan = 0.obs;
final RxInt jumlahSelesai = 0.obs;
// Controller untuk pencarian dan form
final TextEditingController searchController = TextEditingController();
final TextEditingController tindakanController = TextEditingController();
final TextEditingController catatanController = TextEditingController();
// Form key
final GlobalKey<FormState> tindakanFormKey = GlobalKey<FormState>();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadPengaduanData();
}
@override
void onClose() {
searchController.dispose();
tindakanController.dispose();
catatanController.dispose();
super.onClose();
}
Future<void> loadPengaduanData() async {
isLoading.value = true;
try {
final pengaduanData = await _supabaseService.getPengaduan();
if (pengaduanData != null) {
daftarPengaduan.value =
pengaduanData.map((data) => PengaduanModel.fromJson(data)).toList();
// Hitung jumlah berdasarkan status
jumlahDiproses.value =
daftarPengaduan.where((item) => item.status == 'DIPROSES').length;
jumlahTindakan.value =
daftarPengaduan.where((item) => item.status == 'TINDAKAN').length;
jumlahSelesai.value =
daftarPengaduan.where((item) => item.status == 'SELESAI').length;
}
} catch (e) {
print('Error loading pengaduan data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> prosesPengaduan(String pengaduanId) async {
isLoading.value = true;
try {
await _supabaseService.prosesPengaduan(pengaduanId);
await loadPengaduanData();
Get.snackbar(
'Sukses',
'Pengaduan berhasil diproses',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error processing pengaduan: $e');
Get.snackbar(
'Error',
'Gagal memproses pengaduan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> tambahTindakan(String pengaduanId) async {
if (!tindakanFormKey.currentState!.validate()) return;
isLoading.value = true;
try {
final tindakan = TindakanPengaduanModel(
pengaduanId: pengaduanId,
tindakan: tindakanController.text,
catatan: catatanController.text,
tanggalTindakan: DateTime.now(),
petugasId: user?.id,
);
await _supabaseService.tambahTindakanPengaduan(tindakan.toJson());
await _supabaseService.updateStatusPengaduan(pengaduanId, 'TINDAKAN');
// Clear form
tindakanController.clear();
catatanController.clear();
await loadPengaduanData();
Get.back(); // Close dialog
Get.snackbar(
'Sukses',
'Tindakan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error adding tindakan: $e');
Get.snackbar(
'Error',
'Gagal menambahkan tindakan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> selesaikanPengaduan(String pengaduanId) async {
isLoading.value = true;
try {
await _supabaseService.updateStatusPengaduan(pengaduanId, 'SELESAI');
await loadPengaduanData();
Get.snackbar(
'Sukses',
'Pengaduan berhasil diselesaikan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error completing pengaduan: $e');
Get.snackbar(
'Error',
'Gagal menyelesaikan pengaduan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<List<TindakanPengaduanModel>> getTindakanPengaduan(
String pengaduanId) async {
try {
final tindakanData =
await _supabaseService.getTindakanPengaduan(pengaduanId);
if (tindakanData != null) {
return tindakanData
.map((data) => TindakanPengaduanModel.fromJson(data))
.toList();
}
return [];
} catch (e) {
print('Error getting tindakan pengaduan: $e');
return [];
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadPengaduanData();
} finally {
isLoading.value = false;
}
}
void changeCategory(int index) {
selectedCategoryIndex.value = index;
}
List<PengaduanModel> getFilteredPengaduan() {
switch (selectedCategoryIndex.value) {
case 0:
return daftarPengaduan;
case 1:
return daftarPengaduan
.where((item) => item.status == 'DIPROSES')
.toList();
case 2:
return daftarPengaduan
.where((item) => item.status == 'TINDAKAN')
.toList();
case 3:
return daftarPengaduan
.where((item) => item.status == 'SELESAI')
.toList();
default:
return daftarPengaduan;
}
}
// Validasi form
String? validateTindakan(String? value) {
if (value == null || value.isEmpty) {
return 'Tindakan tidak boleh kosong';
}
return null;
}
}

View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:get/get.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/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class PenitipanBantuanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Data untuk penitipan
final RxList<PenitipanBantuanModel> daftarPenitipan =
<PenitipanBantuanModel>[].obs;
final RxInt jumlahMenunggu = 0.obs;
final RxInt jumlahTerverifikasi = 0.obs;
final RxInt jumlahDitolak = 0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadPenitipanData();
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
Future<void> loadPenitipanData() async {
isLoading.value = true;
try {
final penitipanData = await _supabaseService.getPenitipanBantuan();
if (penitipanData != null) {
daftarPenitipan.value = penitipanData
.map((data) => PenitipanBantuanModel.fromJson(data))
.toList();
// Hitung jumlah berdasarkan status
jumlahMenunggu.value =
daftarPenitipan.where((item) => item.status == 'MENUNGGU').length;
jumlahTerverifikasi.value = daftarPenitipan
.where((item) => item.status == 'TERVERIFIKASI')
.length;
jumlahDitolak.value =
daftarPenitipan.where((item) => item.status == 'DITOLAK').length;
}
} catch (e) {
print('Error loading penitipan data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> verifikasiPenitipan(String penitipanId) async {
isLoading.value = true;
try {
await _supabaseService.verifikasiPenitipan(penitipanId);
await loadPenitipanData();
Get.snackbar(
'Sukses',
'Penitipan berhasil diverifikasi',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error verifying penitipan: $e');
Get.snackbar(
'Error',
'Gagal memverifikasi penitipan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> tolakPenitipan(String penitipanId, String alasan) async {
isLoading.value = true;
try {
await _supabaseService.tolakPenitipan(penitipanId, alasan);
await loadPenitipanData();
Get.snackbar(
'Sukses',
'Penitipan berhasil ditolak',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error rejecting penitipan: $e');
Get.snackbar(
'Error',
'Gagal menolak penitipan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<DonaturModel?> getDonaturInfo(String donaturId) async {
try {
final donaturData = await _supabaseService.getDonaturById(donaturId);
if (donaturData != null) {
return DonaturModel.fromJson(donaturData);
}
return null;
} catch (e) {
print('Error getting donatur info: $e');
return null;
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadPenitipanData();
} finally {
isLoading.value = false;
}
}
void changeCategory(int index) {
selectedCategoryIndex.value = index;
}
List<PenitipanBantuanModel> getFilteredPenitipan() {
switch (selectedCategoryIndex.value) {
case 0:
return daftarPenitipan;
case 1:
return daftarPenitipan
.where((item) => item.status == 'MENUNGGU')
.toList();
case 2:
return daftarPenitipan
.where((item) => item.status == 'TERVERIFIKASI')
.toList();
case 3:
return daftarPenitipan
.where((item) => item.status == 'DITOLAK')
.toList();
default:
return daftarPenitipan;
}
}
}

View File

@ -1,5 +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/data/models/desa_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';
@ -8,81 +9,63 @@ class PetugasDesaController 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 RxBool isLoading = false.obs;
final Rx<Map<String, dynamic>?> roleData = Rx<Map<String, dynamic>?>(null);
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Indeks tab yang aktif di bottom navigation bar // Indeks tab yang aktif di bottom navigation bar
final RxInt activeTabIndex = 0.obs; final RxInt activeTabIndex = 0.obs;
// Data untuk dashboard
final RxInt totalPenerima = 0.obs;
final RxInt totalBantuan = 0.obs;
final RxInt totalPenyaluran = 0.obs;
final RxDouble progressPenyaluran = 0.0.obs;
// Data untuk jadwal
final RxList<Map<String, dynamic>> jadwalHariIni =
<Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> jadwalMendatang =
<Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> jadwalSelesai =
<Map<String, dynamic>>[].obs;
// Data untuk permintaan penjadwalan
final RxList<Map<String, dynamic>> permintaanPenjadwalan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahPermintaanPenjadwalan = 0.obs;
// Data untuk notifikasi
final RxList<Map<String, dynamic>> notifikasiBelumDibaca =
<Map<String, dynamic>>[].obs;
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Data untuk inventaris
final RxList<Map<String, dynamic>> daftarInventaris =
<Map<String, dynamic>>[].obs;
final RxDouble totalStok = 0.0.obs;
final RxDouble stokMasuk = 0.0.obs;
final RxDouble stokKeluar = 0.0.obs;
// Data untuk penitipan
final RxList<Map<String, dynamic>> daftarPenitipan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahMenunggu = 0.obs;
final RxInt jumlahTerverifikasi = 0.obs;
final RxInt jumlahDitolak = 0.obs;
// Data untuk pengaduan
final RxList<Map<String, dynamic>> daftarPengaduan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahDiproses = 0.obs;
final RxInt jumlahTindakan = 0.obs;
final RxInt jumlahSelesai = 0.obs;
// Controller untuk pencarian // Controller untuk pencarian
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
// Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
// Model desa dari cache
final Rx<DesaModel?> desaModel = Rx<DesaModel?>(null);
// Counter untuk notifikasi
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Counter untuk permintaan menunggu
final RxInt jumlahMenunggu = 0.obs;
// Counter untuk pengaduan yang diproses
final RxInt jumlahDiproses = 0.obs;
// Data jadwal hari ini
final RxList<dynamic> jadwalHariIni = <dynamic>[].obs;
UserModel? get user => _authController.user; UserModel? get user => _authController.user;
String get role => user?.role ?? 'PETUGASDESA'; String get role => user?.role ?? 'PETUGASDESA';
String get nama => user?.name ?? 'Petugas Desa'; String get nama => user?.name ?? 'Petugas Desa';
// Getter untuk nama lengkap dari profil pengguna
String get namaLengkap => userProfile['name'] ?? user?.name ?? 'Petugas Desa';
// Getter untuk nama desa dari profil pengguna
String get desa {
// Prioritaskan model desa dari user
if (user?.desa != null) {
print('DEBUG: Menggunakan desa dari user model: ${user!.desa!.nama}');
return user!.desa!.nama;
}
// Kemudian coba dari userProfile
if (userProfile['desa'] != null && userProfile['desa'] is Map) {
final desaNama = userProfile['desa']['nama'] ?? 'Desa';
print('DEBUG: Menggunakan desa dari userProfile: $desaNama');
return desaNama;
}
// Fallback ke nilai default
print('DEBUG: Menggunakan nilai default untuk desa');
return userProfile['desa_id'] != null ? 'Desa' : 'Desa';
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
loadUserProfile();
// Inisialisasi manual untuk pengaduan (untuk debugging)
jumlahDiproses.value = 3;
print('onInit - Jumlah pengaduan diproses: ${jumlahDiproses.value}');
loadRoleData();
loadDashboardData();
loadJadwalData();
loadPermintaanPenjadwalanData();
loadNotifikasiData(); loadNotifikasiData();
loadInventarisData(); loadJadwalData();
loadPenitipanData(); loadPenitipanData();
loadPengaduanData(); loadPengaduanData();
} }
@ -93,584 +76,106 @@ class PetugasDesaController extends GetxController {
super.onClose(); super.onClose();
} }
Future<void> loadRoleData() async { // Metode untuk memuat data profil pengguna dari cache
isLoading.value = true; Future<void> loadUserProfile() async {
try { try {
// Jika user sudah ada di AuthController, tidak perlu mengambil data lagi
if (user != null) { if (user != null) {
final data = await _supabaseService.getRoleSpecificData(role); print('DEBUG: User ditemukan di AuthController: ${user!.email}');
roleData.value = data; print('DEBUG: User desa: ${user!.desa?.nama}');
}
} catch (e) {
print('Error loading role data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> loadDashboardData() async { // Ambil data tambahan jika diperlukan, tapi gunakan cache
final profileData = await _supabaseService.getUserProfile();
if (profileData != null) {
print('DEBUG: Profile data ditemukan: ${profileData['name']}');
userProfile.value = profileData;
// Parse data desa jika ada
if (profileData['desa'] != null &&
profileData['desa'] is Map<String, dynamic>) {
try { try {
// Simulasi data untuk dashboard final desaData = profileData['desa'] as Map<String, dynamic>;
await Future.delayed(const Duration(milliseconds: 800)); print('DEBUG: Desa data ditemukan: $desaData');
totalPenerima.value = 120;
totalBantuan.value = 5;
totalPenyaluran.value = 8;
progressPenyaluran.value = 0.75;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getDashboardData();
// totalPenerima.value = result['total_penerima'] ?? 0;
// totalBantuan.value = result['total_bantuan'] ?? 0;
// totalPenyaluran.value = result['total_penyaluran'] ?? 0;
// progressPenyaluran.value = result['progress_penyaluran'] ?? 0.0;
} catch (e) { } catch (e) {
print('Error loading dashboard data: $e'); print('Error parsing desa data: $e');
} }
} else {
print('DEBUG: Desa data tidak ditemukan atau bukan Map');
}
} else {
print('DEBUG: Profile data tidak ditemukan');
}
} else {
print('DEBUG: User tidak ditemukan di AuthController');
} }
Future<void> loadJadwalData() async {
try {
// Simulasi data untuk jadwal
await Future.delayed(const Duration(milliseconds: 600));
jadwalHariIni.value = [
{
'id': '1',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '15 April 2023',
'waktu': '09:00 - 12:00',
'status': 'aktif',
'jumlah_penerima': 45,
},
{
'id': '2',
'lokasi': 'Pos RW 03',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '15 April 2023',
'waktu': '13:00 - 15:00',
'status': 'aktif',
'jumlah_penerima': 30,
},
];
jadwalMendatang.value = [
{
'id': '3',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '22 April 2023',
'waktu': '09:00 - 12:00',
'status': 'terjadwal',
'jumlah_penerima': 50,
},
{
'id': '4',
'lokasi': 'Pos RW 05',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '23 April 2023',
'waktu': '13:00 - 15:00',
'status': 'terjadwal',
'jumlah_penerima': 35,
},
];
jadwalSelesai.value = [
{
'id': '5',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '8 April 2023',
'waktu': '09:00 - 12:00',
'status': 'selesai',
'jumlah_penerima': 48,
},
{
'id': '6',
'lokasi': 'Pos RW 02',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '9 April 2023',
'waktu': '13:00 - 15:00',
'status': 'selesai',
'jumlah_penerima': 32,
},
];
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getJadwalData();
// jadwalHariIni.value = result['hari_ini'] ?? [];
// jadwalMendatang.value = result['mendatang'] ?? [];
// jadwalSelesai.value = result['selesai'] ?? [];
} catch (e) { } catch (e) {
print('Error loading jadwal data: $e'); print('Error loading user profile: $e');
}
}
Future<void> loadPermintaanPenjadwalanData() async {
try {
// Simulasi data untuk permintaan penjadwalan
await Future.delayed(const Duration(milliseconds: 600));
permintaanPenjadwalan.value = [
{
'id': '1',
'nama': 'Ahmad Sulaiman',
'nik': '3201234567890001',
'jenis_bantuan': 'Beras',
'tanggal_permintaan': '14 April 2023',
'alamat': 'Dusun Sukamaju RT 02/03',
'status': 'menunggu',
},
{
'id': '2',
'nama': 'Siti Aminah',
'nik': '3201234567890002',
'jenis_bantuan': 'Sembako',
'tanggal_permintaan': '13 April 2023',
'alamat': 'Dusun Sukamaju RT 01/03',
'status': 'menunggu',
},
];
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPermintaanPenjadwalanData();
// permintaanPenjadwalan.value = result ?? [];
// jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
} catch (e) {
print('Error loading permintaan penjadwalan data: $e');
} }
} }
// Metode untuk memuat data notifikasi
Future<void> loadNotifikasiData() async { Future<void> loadNotifikasiData() async {
try { try {
// Simulasi data untuk notifikasi if (user != null) {
await Future.delayed(const Duration(milliseconds: 500)); final notifikasiData =
await _supabaseService.getNotifikasiBelumDibaca(user!.id);
// Hitung jumlah notifikasi yang belum dibaca if (notifikasiData != null) {
final List<Map<String, dynamic>> notifikasi = [ jumlahNotifikasiBelumDibaca.value = notifikasiData.length;
{ }
'id': '1', }
'judul': 'Jadwal Penyaluran Baru',
'pesan': 'Jadwal penyaluran beras telah ditambahkan untuk hari ini',
'waktu': '08:30',
'dibaca': false,
'tanggal': 'hari_ini',
},
{
'id': '2',
'judul': 'Pengajuan Bantuan Baru',
'pesan': 'Ada 3 pengajuan bantuan baru yang perlu diverifikasi',
'waktu': '10:15',
'dibaca': false,
'tanggal': 'hari_ini',
},
{
'id': '3',
'judul': 'Laporan Penyaluran',
'pesan':
'Laporan penyaluran bantuan tanggal 14 April 2023 telah selesai',
'waktu': '16:45',
'dibaca': true,
'tanggal': 'kemarin',
},
];
notifikasiBelumDibaca.value =
notifikasi.where((n) => n['dibaca'] == false).toList();
jumlahNotifikasiBelumDibaca.value = notifikasiBelumDibaca.length;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getNotifikasiData();
// notifikasiBelumDibaca.value = result.where((n) => n['dibaca'] == false).toList();
// jumlahNotifikasiBelumDibaca.value = notifikasiBelumDibaca.length;
} catch (e) { } catch (e) {
print('Error loading notifikasi data: $e'); print('Error loading notifikasi data: $e');
} }
} }
Future<void> loadInventarisData() async { // Metode untuk memuat data jadwal
Future<void> loadJadwalData() async {
try { try {
// Simulasi data untuk inventaris final jadwalHariIniData = await _supabaseService.getJadwalHariIni();
await Future.delayed(const Duration(milliseconds: 700)); if (jadwalHariIniData != null) {
jadwalHariIni.value = jadwalHariIniData;
daftarInventaris.value = [ }
{
'id': '1',
'nama': 'Beras',
'jenis': 'Sembako',
'stok': '750 kg',
'stok_angka': 750.0,
'lokasi': 'Gudang Utama',
'tanggal_masuk': '10 April 2023',
'kadaluarsa': '10 April 2024',
},
{
'id': '2',
'nama': 'Minyak Goreng',
'jenis': 'Sembako',
'stok': '250 liter',
'stok_angka': 250.0,
'lokasi': 'Gudang Utama',
'tanggal_masuk': '12 April 2023',
'kadaluarsa': '12 Oktober 2023',
},
{
'id': '3',
'nama': 'Paket Sembako',
'jenis': 'Paket Bantuan',
'stok': '100 paket',
'stok_angka': 100.0,
'lokasi': 'Gudang Cabang',
'tanggal_masuk': '15 April 2023',
'kadaluarsa': '15 Juli 2023',
},
];
// Hitung total stok, stok masuk, dan stok keluar
totalStok.value = daftarInventaris.fold(
0, (sum, item) => sum + (item['stok_angka'] as double));
stokMasuk.value = 500.0; // Contoh data
stokKeluar.value = 350.0; // Contoh data
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getInventarisData();
// daftarInventaris.value = result['daftar'] ?? [];
// totalStok.value = result['total_stok'] ?? 0.0;
// stokMasuk.value = result['stok_masuk'] ?? 0.0;
// stokKeluar.value = result['stok_keluar'] ?? 0.0;
} catch (e) { } catch (e) {
print('Error loading inventaris data: $e'); print('Error loading jadwal data: $e');
} }
} }
// Metode untuk memuat data penitipan
Future<void> loadPenitipanData() async { Future<void> loadPenitipanData() async {
try { try {
// Simulasi data untuk penitipan // Simulasi data untuk contoh
await Future.delayed(const Duration(milliseconds: 600)); jumlahMenunggu.value = 3;
daftarPenitipan.value = [
{
'id': '1',
'donatur': 'PT Sejahtera Abadi',
'jenis_bantuan': 'Sembako',
'jumlah': '500 kg',
'tanggal_pengajuan': '15 April 2023',
'status': 'Menunggu',
},
{
'id': '2',
'donatur': 'Yayasan Peduli Sesama',
'jenis_bantuan': 'Pakaian',
'jumlah': '200 pcs',
'tanggal_pengajuan': '14 April 2023',
'status': 'Terverifikasi',
},
{
'id': '3',
'donatur': 'Bank BRI',
'jenis_bantuan': 'Beras',
'jumlah': '300 kg',
'tanggal_pengajuan': '13 April 2023',
'status': 'Terverifikasi',
},
{
'id': '4',
'donatur': 'Komunitas Peduli',
'jenis_bantuan': 'Alat Tulis',
'jumlah': '100 set',
'tanggal_pengajuan': '12 April 2023',
'status': 'Ditolak',
},
];
// Hitung jumlah penitipan berdasarkan status
jumlahMenunggu.value =
daftarPenitipan.where((p) => p['status'] == 'Menunggu').length;
jumlahTerverifikasi.value =
daftarPenitipan.where((p) => p['status'] == 'Terverifikasi').length;
jumlahDitolak.value =
daftarPenitipan.where((p) => p['status'] == 'Ditolak').length;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPenitipanData();
// daftarPenitipan.value = result ?? [];
// jumlahMenunggu.value = daftarPenitipan.where((p) => p['status'] == 'Menunggu').length;
// jumlahTerverifikasi.value = daftarPenitipan.where((p) => p['status'] == 'Terverifikasi').length;
// jumlahDitolak.value = daftarPenitipan.where((p) => p['status'] == 'Ditolak').length;
} catch (e) { } catch (e) {
print('Error loading penitipan data: $e'); print('Error loading penitipan data: $e');
} }
} }
// Metode untuk memuat data pengaduan
Future<void> loadPengaduanData() async { Future<void> loadPengaduanData() async {
try { try {
// Simulasi data untuk pengaduan // Simulasi data untuk contoh
await Future.delayed(const Duration(milliseconds: 650)); jumlahDiproses.value = 2;
// Pastikan data pengaduan tidak kosong
daftarPengaduan.value = [
{
'id': '1',
'nama': 'Budi Santoso',
'nik': '3201020107030011',
'jenis_pengaduan': 'Bantuan Tidak Diterima',
'deskripsi':
'Saya belum menerima bantuan beras yang dijadwalkan minggu lalu',
'tanggal': '15 April 2023',
'status': 'Diproses',
},
{
'id': '2',
'nama': 'Siti Rahayu',
'nik': '3201020107030010',
'jenis_pengaduan': 'Kualitas Bantuan',
'deskripsi':
'Beras yang diterima berkualitas buruk dan tidak layak konsumsi',
'tanggal': '14 April 2023',
'status': 'Tindakan',
'tindakan':
'Pengecekan kualitas beras di gudang dan pengambilan sampel',
},
{
'id': '3',
'nama': 'Ahmad Fauzi',
'nik': '3201020107030013',
'jenis_pengaduan': 'Jumlah Bantuan',
'deskripsi':
'Jumlah bantuan yang diterima tidak sesuai dengan yang dijanjikan',
'tanggal': '13 April 2023',
'status': 'Tindakan',
'tindakan':
'Verifikasi data penerima dan jumlah bantuan yang seharusnya diterima',
},
{
'id': '4',
'nama': 'Dewi Lestari',
'nik': '3201020107030012',
'jenis_pengaduan': 'Jadwal Penyaluran',
'deskripsi':
'Jadwal penyaluran bantuan sering berubah tanpa pemberitahuan',
'tanggal': '10 April 2023',
'status': 'Selesai',
'tindakan':
'Koordinasi dengan tim penyaluran untuk perbaikan sistem pemberitahuan',
'hasil':
'Implementasi sistem notifikasi perubahan jadwal melalui SMS dan pengumuman di balai desa',
},
// Tambahkan data pengaduan dengan status 'Diproses' untuk memastikan counter muncul
{
'id': '5',
'nama': 'Joko Widodo',
'nik': '3201020107030014',
'jenis_pengaduan': 'Bantuan Tidak Sesuai',
'deskripsi':
'Bantuan yang diterima tidak sesuai dengan yang dijanjikan',
'tanggal': '16 April 2023',
'status': 'Diproses',
},
{
'id': '6',
'nama': 'Anita Sari',
'nik': '3201020107030015',
'jenis_pengaduan': 'Bantuan Tidak Tepat Sasaran',
'deskripsi':
'Bantuan diberikan kepada warga yang tidak berhak menerima',
'tanggal': '17 April 2023',
'status': 'Diproses',
},
];
// Hitung jumlah pengaduan berdasarkan status
int jumlahDiprosesTemp =
daftarPengaduan.where((p) => p['status'] == 'Diproses').length;
int jumlahTindakanTemp =
daftarPengaduan.where((p) => p['status'] == 'Tindakan').length;
int jumlahSelesaiTemp =
daftarPengaduan.where((p) => p['status'] == 'Selesai').length;
// Update nilai Rx
jumlahDiproses.value = jumlahDiprosesTemp;
jumlahTindakan.value = jumlahTindakanTemp;
jumlahSelesai.value = jumlahSelesaiTemp;
// Print untuk debugging
print('Data pengaduan dimuat:');
print('Jumlah pengaduan diproses: ${jumlahDiproses.value}');
print('Jumlah pengaduan tindakan: ${jumlahTindakan.value}');
print('Jumlah pengaduan selesai: ${jumlahSelesai.value}');
print('Total pengaduan: ${daftarPengaduan.length}');
// Perbarui UI secara manual
update();
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPengaduanData();
// daftarPengaduan.value = result ?? [];
// jumlahDiproses.value = daftarPengaduan.where((p) => p['status'] == 'Diproses').length;
// jumlahTindakan.value = daftarPengaduan.where((p) => p['status'] == 'Tindakan').length;
// jumlahSelesai.value = daftarPengaduan.where((p) => p['status'] == 'Selesai').length;
} catch (e) { } catch (e) {
print('Error loading pengaduan data: $e'); print('Error loading pengaduan data: $e');
} }
} }
// Method untuk memperbarui jumlah pengaduan secara manual (untuk debugging) // Metode untuk memperbarui counter pengaduan
void updatePengaduanCounter() { Future<void> updatePengaduanCounter() async {
jumlahDiproses.value = 5; // Set nilai secara manual try {
update(); // Perbarui UI await loadPengaduanData();
print( } catch (e) {
'Counter pengaduan diperbarui secara manual: ${jumlahDiproses.value}'); print('Error updating pengaduan counter: $e');
} }
void tandaiNotifikasiDibaca(String id) {
// Implementasi untuk menandai notifikasi sebagai dibaca
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status notifikasi
// await _supabaseService.markNotificationAsRead(id);
// Perbarui data lokal
loadNotifikasiData();
}
void tambahInventaris(Map<String, dynamic> data) {
// Implementasi untuk menambah inventaris
// Di implementasi nyata, akan memanggil Supabase untuk menambah data inventaris
// await _supabaseService.addInventory(data);
// Perbarui data lokal
loadInventarisData();
}
void hapusInventaris(String id) {
// Implementasi untuk menghapus inventaris
// Di implementasi nyata, akan memanggil Supabase untuk menghapus data inventaris
// await _supabaseService.deleteInventory(id);
// Perbarui data lokal
loadInventarisData();
}
void terimaPermohonanPenitipan(String id) {
// Implementasi untuk menerima permohonan penitipan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status penitipan
// await _supabaseService.acceptDeposit(id);
// Perbarui data lokal
loadPenitipanData();
loadInventarisData(); // Perbarui inventaris karena ada penambahan stok
}
void tolakPermohonanPenitipan(String id) {
// Implementasi untuk menolak permohonan penitipan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status penitipan
// await _supabaseService.rejectDeposit(id);
// Perbarui data lokal
loadPenitipanData();
}
void prosesPengaduan(String id, String tindakan) {
// Implementasi untuk memproses pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.processPengaduan(id, tindakan);
// Perbarui data lokal
loadPengaduanData();
}
void selesaikanPengaduan(String id, String hasil) {
// Implementasi untuk menyelesaikan pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.completePengaduan(id, hasil);
// Perbarui data lokal
loadPengaduanData();
}
void logout() {
_authController.logout();
} }
// Metode untuk mengubah tab aktif
void changeTab(int index) { void changeTab(int index) {
activeTabIndex.value = index; activeTabIndex.value = index;
} }
// Metode untuk konfirmasi permintaan penjadwalan // Metode untuk logout
Future<void> konfirmasiPermintaanPenjadwalan( Future<void> logout() async {
String id, String jadwalId) async { await _authController.logout();
try {
if (id.isEmpty || jadwalId.isEmpty) {
Get.snackbar(
'Error',
'ID permintaan atau jadwal tidak valid',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
// Simulasi proses konfirmasi
await Future.delayed(const Duration(milliseconds: 800));
// Hapus permintaan dari daftar
permintaanPenjadwalan.removeWhere((item) => item['id'] == id);
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diupdate ke Supabase
// await _supabaseService.konfirmasiPermintaanPenjadwalan(id, jadwalId);
// await loadPermintaanPenjadwalanData();
// await loadJadwalData();
} catch (e) {
print('Error konfirmasi permintaan penjadwalan: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat mengkonfirmasi permintaan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Metode untuk menolak permintaan penjadwalan
Future<void> tolakPermintaanPenjadwalan(String id, String alasan) async {
try {
if (id.isEmpty) {
Get.snackbar(
'Error',
'ID permintaan tidak valid',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
// Simulasi proses penolakan
await Future.delayed(const Duration(milliseconds: 800));
// Hapus permintaan dari daftar
permintaanPenjadwalan.removeWhere((item) => item['id'] == id);
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diupdate ke Supabase
// await _supabaseService.tolakPermintaanPenjadwalan(id, alasan);
// await loadPermintaanPenjadwalanData();
} catch (e) {
print('Error tolak permintaan penjadwalan: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat menolak permintaan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
} }
} }

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class PetugasDesaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
// Data untuk dashboard
final RxInt totalPenerima = 0.obs;
final RxInt totalBantuan = 0.obs;
final RxInt totalPenyaluran = 0.obs;
final RxDouble progressPenyaluran = 0.0.obs;
// Data untuk notifikasi
final RxList<NotifikasiModel> notifikasiBelumDibaca = <NotifikasiModel>[].obs;
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
UserModel? get user => _authController.user;
String get role => user?.role ?? 'PETUGASDESA';
String get nama => user?.name ?? 'Petugas Desa';
// Getter untuk nama lengkap dari profil pengguna
String get namaLengkap => userProfile['name'] ?? user?.name ?? 'Petugas Desa';
// Getter untuk nama desa dari profil pengguna
String get desa =>
userProfile['desa']?['nama'] ??
(userProfile['desa_id'] != null ? 'Desa' : 'Desa');
@override
void onInit() {
super.onInit();
loadUserProfile();
loadDashboardData();
loadNotifikasiData();
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
// Metode untuk memuat data profil pengguna dari cache
Future<void> loadUserProfile() async {
try {
// Jika user sudah ada di AuthController, tidak perlu mengambil data lagi
if (user != null) {
// Ambil data tambahan jika diperlukan, tapi gunakan cache
final profileData = await _supabaseService.getUserProfile();
if (profileData != null) {
userProfile.value = profileData;
}
}
} catch (e) {
print('Error loading user profile: $e');
}
}
Future<void> loadDashboardData() async {
isLoading.value = true;
try {
// Mengambil data total penerima
final penerimaData = await _supabaseService.getTotalPenerima();
totalPenerima.value = penerimaData ?? 0;
// Mengambil data total bantuan
final bantuanData = await _supabaseService.getTotalBantuan();
totalBantuan.value = bantuanData ?? 0;
// Mengambil data total penyaluran
final penyaluranData = await _supabaseService.getTotalPenyaluran();
totalPenyaluran.value = penyaluranData ?? 0;
// Menghitung progress penyaluran
if (totalBantuan.value > 0) {
progressPenyaluran.value =
(totalPenyaluran.value / totalBantuan.value) * 100;
} else {
progressPenyaluran.value = 0.0;
}
} catch (e) {
print('Error loading dashboard data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> loadNotifikasiData() async {
try {
final notifikasiData =
await _supabaseService.getNotifikasiBelumDibaca(user?.id ?? '');
if (notifikasiData != null) {
notifikasiBelumDibaca.value = notifikasiData
.map((data) => NotifikasiModel.fromJson(data))
.toList();
jumlahNotifikasiBelumDibaca.value = notifikasiBelumDibaca.length;
}
} catch (e) {
print('Error loading notifikasi data: $e');
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadDashboardData();
await loadNotifikasiData();
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:get/get.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';
class StokBantuanController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
// Data untuk stok bantuan
final RxList<StokBantuanModel> daftarStokBantuan = <StokBantuanModel>[].obs;
final RxDouble totalStok = 0.0.obs;
final RxDouble stokMasuk = 0.0.obs;
final RxDouble stokKeluar = 0.0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
final RxString searchQuery = ''.obs;
UserModel? get user => _authController.user;
@override
void onInit() {
super.onInit();
loadStokBantuanData();
// Listener untuk pencarian
searchController.addListener(() {
searchQuery.value = searchController.text;
});
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
Future<void> loadStokBantuanData() async {
isLoading.value = true;
try {
final stokBantuanData = await _supabaseService.getStokBantuan();
if (stokBantuanData != null) {
daftarStokBantuan.value = stokBantuanData
.map((data) => StokBantuanModel.fromJson(data))
.toList();
// Hitung total stok
totalStok.value = 0;
for (var item in daftarStokBantuan) {
totalStok.value += item.jumlah ?? 0;
}
// Ambil data stok masuk dan keluar
final stokData = await _supabaseService.getStokStatistics();
if (stokData != null) {
stokMasuk.value = stokData['masuk'] ?? 0;
stokKeluar.value = stokData['keluar'] ?? 0;
}
}
} catch (e) {
print('Error loading stok bantuan data: $e');
} finally {
isLoading.value = false;
}
}
Future<void> addStok(StokBantuanModel stok) async {
isLoading.value = true;
try {
await _supabaseService.addStok(stok.toJson());
await loadStokBantuanData();
Get.snackbar(
'Sukses',
'Stok berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error adding stok: $e');
Get.snackbar(
'Error',
'Gagal menambahkan stok: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> updateStok(StokBantuanModel stok) async {
isLoading.value = true;
try {
await _supabaseService.updateStok(stok.id ?? '', stok.toJson());
await loadStokBantuanData();
Get.snackbar(
'Sukses',
'Stok berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error updating stok: $e');
Get.snackbar(
'Error',
'Gagal memperbarui stok: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> deleteStok(String stokId) async {
isLoading.value = true;
try {
await _supabaseService.deleteStok(stokId);
await loadStokBantuanData();
Get.snackbar(
'Sukses',
'Stok berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
print('Error deleting stok: $e');
Get.snackbar(
'Error',
'Gagal menghapus stok: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadStokBantuanData();
} finally {
isLoading.value = false;
}
}
List<StokBantuanModel> getFilteredStokBantuan() {
if (searchQuery.isEmpty) {
return daftarStokBantuan;
} else {
return daftarStokBantuan
.where((item) =>
(item.nama
?.toLowerCase()
.contains(searchQuery.value.toLowerCase()) ??
false) ||
(item.satuan
?.toLowerCase()
.contains(searchQuery.value.toLowerCase()) ??
false) ||
(item.deskripsi
?.toLowerCase()
.contains(searchQuery.value.toLowerCase()) ??
false))
.toList();
}
}
}

View File

@ -22,9 +22,9 @@ class DashboardView extends GetView<PetugasDesaController> {
children: [ children: [
// Header dengan greeting // Header dengan greeting
GreetingHeader( GreetingHeader(
name: controller.roleData.value?['namaLengkap'] ?? 'Ahmad', name: controller.namaLengkap,
role: 'Petugas Desa', role: 'Petugas Desa',
desa: controller.roleData.value?['Desa'] ?? 'Jatihurip', desa: controller.desa,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),

View File

@ -1,387 +0,0 @@
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/theme/app_theme.dart';
class InventarisView extends GetView<PetugasDesaController> {
const InventarisView({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan inventaris
_buildInventarisSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
const SizedBox(height: 20),
// Daftar inventaris
_buildInventarisList(context),
],
),
),
);
}
Widget _buildInventarisSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Inventaris',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Total Stok',
value: '1,250 kg',
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.input,
title: 'Masuk Bulan Ini',
value: '500 kg',
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.output,
title: 'Keluar Bulan Ini',
value: '350 kg',
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Cari bantuan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () {
// Tampilkan dialog filter
},
icon: const Icon(Icons.filter_list),
tooltip: 'Filter',
),
),
],
);
}
Widget _buildInventarisList(BuildContext context) {
final List<Map<String, dynamic>> inventarisList = [
{
'nama': 'Beras',
'jenis': 'Sembako',
'stok': '750 kg',
'lokasi': 'Gudang Utama',
'tanggal_masuk': '10 April 2023',
'kadaluarsa': '10 April 2024',
},
{
'nama': 'Minyak Goreng',
'jenis': 'Sembako',
'stok': '250 liter',
'lokasi': 'Gudang Utama',
'tanggal_masuk': '12 April 2023',
'kadaluarsa': '12 Oktober 2023',
},
{
'nama': 'Paket Sembako',
'jenis': 'Paket Bantuan',
'stok': '100 paket',
'lokasi': 'Gudang Cabang',
'tanggal_masuk': '15 April 2023',
'kadaluarsa': '15 Juli 2023',
},
{
'nama': 'Selimut',
'jenis': 'Non-Pangan',
'stok': '150 buah',
'lokasi': 'Gudang Cabang',
'tanggal_masuk': '5 April 2023',
'kadaluarsa': '-',
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Inventaris',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton.icon(
onPressed: () {
// Navigasi ke halaman tambah inventaris
},
icon: const Icon(Icons.add),
label: const Text('Tambah'),
style: TextButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 12),
...inventarisList.map((item) => _buildInventarisItem(context, item)),
],
);
}
Widget _buildInventarisItem(BuildContext context, Map<String, dynamic> item) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item['nama'] ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item['jenis'] ?? '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Stok',
value: item['stok'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.location_on_outlined,
label: 'Lokasi',
value: item['lokasi'] ?? '',
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Masuk',
value: item['tanggal_masuk'] ?? '',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.timelapse,
label: 'Kadaluarsa',
value: item['kadaluarsa'] ?? '',
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Tampilkan detail inventaris
},
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}

View File

@ -118,8 +118,8 @@ class NotifikasiView extends GetView<PetugasDesaController> {
); );
}, },
backgroundColor: AppTheme.primaryColor, backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.done_all),
tooltip: 'Tandai Semua Dibaca', tooltip: 'Tandai Semua Dibaca',
child: const Icon(Icons.done_all),
), ),
); );
} }

View File

@ -237,8 +237,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
// Daftar penerima // Daftar penerima
...daftarPenerima ...daftarPenerima
.map((penerima) => _buildPenerimaItem(context, penerima)) .map((penerima) => _buildPenerimaItem(context, penerima)),
.toList(),
], ],
), ),
); );

View File

@ -1,11 +1,11 @@
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/modules/petugas_desa/controllers/jadwal_penyaluran_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/modules/petugas_desa/components/jadwal_section_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart';
class PenyaluranView extends GetView<PetugasDesaController> { class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key}); const PenyaluranView({super.key});
@override @override

View File

@ -1,16 +1,17 @@
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/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
class PermintaanPenjadwalanView extends GetView<PetugasDesaController> { class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
const PermintaanPenjadwalanView({Key? key}) : super(key: key); const PermintaanPenjadwalanView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Pastikan controller sudah diinisialisasi // Pastikan controller sudah diinisialisasi
if (!Get.isRegistered<PetugasDesaController>()) { if (!Get.isRegistered<JadwalPenyaluranController>()) {
Get.put(PetugasDesaController()); Get.put(JadwalPenyaluranController());
} }
return Scaffold( return Scaffold(
@ -236,7 +237,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
); );
} }
Widget _buildPermintaanItem(BuildContext context, Map<String, dynamic> item) { Widget _buildPermintaanItem(BuildContext context, PenyaluranBantuanModel item) {
Color statusColor = Colors.orange; Color statusColor = Colors.orange;
IconData statusIcon = Icons.pending_actions; IconData statusIcon = Icons.pending_actions;
@ -265,7 +266,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
item['nama'] ?? '', item.judul ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -307,8 +308,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.person, icon: Icons.person,
label: 'NIK', label: 'ID',
value: item['nik'] ?? '', value: item.id ?? '',
), ),
), ),
Expanded( Expanded(
@ -316,7 +317,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
context, context,
icon: Icons.category, icon: Icons.category,
label: 'Jenis Bantuan', label: 'Jenis Bantuan',
value: item['jenis_bantuan'] ?? '', value: item.judul ?? '',
), ),
), ),
], ],
@ -329,15 +330,15 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
context, context,
icon: Icons.calendar_today, icon: Icons.calendar_today,
label: 'Tanggal Permintaan', label: 'Tanggal Permintaan',
value: item['tanggal_permintaan'] ?? '', value: item.createdAt?.toString().substring(0, 10) ?? '',
), ),
), ),
Expanded( Expanded(
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.location_on, icon: Icons.location_on,
label: 'Alamat', label: 'Deskripsi',
value: item['alamat'] ?? '', value: item.deskripsi ?? '',
), ),
), ),
], ],
@ -420,15 +421,15 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
} }
// Dialog untuk konfirmasi permintaan // Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) { void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) {
String? selectedJadwalId; String? selectedJadwalId;
// Data jadwal yang tersedia dari controller // Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) { final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: jadwal['id'], value: jadwal.id,
child: Text( child: Text(
'${jadwal['tanggal'] ?? ''} - ${jadwal['lokasi'] ?? ''} (${jadwal['jenis_bantuan'] ?? ''})'), '${jadwal.tanggalPenjadwalan?.toString().substring(0, 10) ?? ''} - ${jadwal.lokasiPenyaluranId ?? ''} (${jadwal.judul ?? ''})'),
); );
}).toList(); }).toList();
@ -448,7 +449,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'), 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('Pilih jadwal penyaluran:'), const Text('Pilih jadwal penyaluran:'),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -474,9 +475,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
onPressed: () { onPressed: () {
if (selectedJadwalId != null) { if (selectedJadwalId != null) {
// Panggil metode konfirmasi di controller // Panggil metode konfirmasi di controller
controller.konfirmasiPermintaanPenjadwalan( controller.approveJadwal(
permintaan['id'] ?? '', permintaan.id ?? '',
selectedJadwalId ?? '',
); );
Get.back(); Get.back();
@ -508,7 +508,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
} }
// Dialog untuk menolak permintaan // Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) { void _showTolakDialog(PenyaluranBantuanModel permintaan) {
final TextEditingController alasanController = TextEditingController(); final TextEditingController alasanController = TextEditingController();
Get.dialog( Get.dialog(
@ -519,7 +519,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'), 'Anda akan menolak permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('Alasan penolakan:'), const Text('Alasan penolakan:'),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -542,8 +542,8 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
onPressed: () { onPressed: () {
if (alasanController.text.trim().isNotEmpty) { if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller // Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan( controller.rejectJadwal(
permintaan['id'] ?? '', permintaan.id ?? '',
alasanController.text.trim(), alasanController.text.trim(),
); );

View File

@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/notifikasi_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/inventaris_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/stok_bantuan_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/penitipan_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/pengaduan_view.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
@ -35,7 +35,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 3: case 3:
return const Text('Pengaduan'); return const Text('Pengaduan');
case 4: case 4:
return const Text('Inventaris'); return const Text('Stok Bantuan');
default: default:
return const Text('Petugas Desa'); return const Text('Petugas Desa');
} }
@ -94,7 +94,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
], ],
); );
// Tombol tambah untuk jadwal dan inventaris // Tombol tambah untuk jadwal dan stok bantuan
if (activeTab == 1) { if (activeTab == 1) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -115,9 +115,9 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Tambah Inventaris', tooltip: 'Tambah Stok Bantuan',
onPressed: () { onPressed: () {
// Implementasi untuk menambah inventaris baru // Implementasi untuk menambah stok bantuan baru
}, },
), ),
notificationButton, notificationButton,
@ -171,7 +171,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 3: case 3:
return const PengaduanView(); return const PengaduanView();
case 4: case 4:
return const InventarisView(); return const StokBantuanView();
default: default:
return const DashboardView(); return const DashboardView();
} }
@ -325,7 +325,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
}), }),
Obx(() => ListTile( Obx(() => ListTile(
leading: const Icon(Icons.inventory_2_outlined), leading: const Icon(Icons.inventory_2_outlined),
title: const Text('Inventaris'), title: const Text('Stok Bantuan'),
selected: controller.activeTabIndex.value == 4, selected: controller.activeTabIndex.value == 4,
selectedColor: AppTheme.primaryColor, selectedColor: AppTheme.primaryColor,
onTap: () { onTap: () {
@ -390,6 +390,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
onTap: () { onTap: () {
// Navigasi ke halaman profil // Navigasi ke halaman profil
Navigator.pop(context); Navigator.pop(context);
Get.toNamed('/profile');
}, },
), ),
ListTile( ListTile(
@ -623,7 +624,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
const BottomNavigationBarItem( const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined), icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2), activeIcon: Icon(Icons.inventory_2),
label: 'Inventaris', label: 'Stok Bantuan',
), ),
], ],
); );

View File

@ -0,0 +1,768 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/stok_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class StokBantuanView extends GetView<StokBantuanController> {
const StokBantuanView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: controller.refreshData,
child: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: _buildContent(context)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Tampilkan dialog tambah stok bantuan
_showAddStokDialog(context);
},
backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.add),
),
);
}
Widget _buildContent(BuildContext context) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan stok bantuan
_buildStokBantuanSummary(context),
const SizedBox(height: 24),
// Filter dan pencarian
_buildFilterSearch(context),
const SizedBox(height: 20),
// Daftar stok bantuan
_buildStokBantuanList(context),
],
),
),
);
}
Widget _buildStokBantuanSummary(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ringkasan Stok Bantuan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.inventory_2_outlined,
title: 'Total Stok',
value: DateFormatter.formatNumber(controller.totalStok.value),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.input,
title: 'Masuk',
value: DateFormatter.formatNumber(controller.stokMasuk.value),
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.output,
title: 'Keluar',
value:
DateFormatter.formatNumber(controller.stokKeluar.value),
),
),
],
),
],
),
);
}
Widget _buildSummaryItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
}) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildFilterSearch(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari bantuan...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: controller.refreshData,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
),
],
);
}
Widget _buildStokBantuanList(BuildContext context) {
return Obx(() {
final filteredList = controller.getFilteredStokBantuan();
if (filteredList.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
const Icon(Icons.inventory_2_outlined,
size: 80, color: Colors.grey),
const SizedBox(height: 16),
Text(
controller.searchQuery.isEmpty
? 'Belum ada data stok bantuan'
: 'Tidak ada stok bantuan yang sesuai dengan pencarian',
style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Stok Bantuan',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${filteredList.length} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
const SizedBox(height: 12),
...filteredList.map((item) => _buildStokBantuanItem(context, item)),
],
);
});
}
Widget _buildStokBantuanItem(BuildContext context, StokBantuanModel item) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item.nama ?? 'Tanpa Nama',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.status ?? 'TERSEDIA',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
if (item.deskripsi != null && item.deskripsi!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
item.deskripsi!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.inventory,
label: 'Jumlah',
value:
'${DateFormatter.formatNumber(item.jumlah)} ${item.satuan ?? ''}',
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.calendar_today,
label: 'Tanggal Masuk',
value: DateFormatter.formatDate(item.tanggalMasuk),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildItemDetail(
context,
icon: Icons.timelapse,
label: 'Kadaluarsa',
value: DateFormatter.formatDate(item.tanggalKadaluarsa),
),
),
Expanded(
child: _buildItemDetail(
context,
icon: Icons.access_time,
label: 'Terakhir Diperbarui',
value: DateFormatter.formatDate(item.updatedAt),
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
// Tampilkan dialog edit stok bantuan
_showEditStokDialog(context, item);
},
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit'),
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
TextButton.icon(
onPressed: () {
// Tampilkan dialog konfirmasi hapus
_showDeleteConfirmation(context, item);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
),
],
),
],
),
),
);
}
Widget _buildItemDetail(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
void _showAddStokDialog(BuildContext context) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController();
final jumlahController = TextEditingController();
final satuanController = TextEditingController();
final deskripsiController = TextEditingController();
DateTime? tanggalMasuk = DateTime.now();
DateTime? tanggalKadaluarsa;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tambah Stok Bantuan'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: namaController,
decoration: const InputDecoration(
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: jumlahController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: deskripsiController,
decoration: const InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalMasuk = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalMasuk),
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalKadaluarsa = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
final stok = StokBantuanModel(
nama: namaController.text,
jumlah: double.parse(jumlahController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: 'TERSEDIA',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
controller.addStok(stok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showEditStokDialog(BuildContext context, StokBantuanModel stok) {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController(text: stok.nama);
final jumlahController =
TextEditingController(text: stok.jumlah?.toString());
final satuanController = TextEditingController(text: stok.satuan);
final deskripsiController = TextEditingController(text: stok.deskripsi);
DateTime? tanggalMasuk = stok.tanggalMasuk;
DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Stok Bantuan'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: namaController,
decoration: const InputDecoration(
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: jumlahController,
decoration: const InputDecoration(
labelText: 'Jumlah',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jumlah tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Jumlah harus berupa angka';
}
return null;
},
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: TextFormField(
controller: satuanController,
decoration: const InputDecoration(
labelText: 'Satuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Satuan tidak boleh kosong';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: deskripsiController,
decoration: const InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalMasuk ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalMasuk = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalMasuk),
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: tanggalKadaluarsa ??
DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime(2030),
);
if (picked != null) {
tanggalKadaluarsa = picked;
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDate(tanggalKadaluarsa),
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
final updatedStok = StokBantuanModel(
id: stok.id,
nama: namaController.text,
jumlah: double.parse(jumlahController.text),
satuan: satuanController.text,
deskripsi: deskripsiController.text,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: stok.status,
createdAt: stok.createdAt,
updatedAt: DateTime.now(),
);
controller.updateStok(updatedStok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
),
);
}
void _showDeleteConfirmation(BuildContext context, StokBantuanModel stok) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Konfirmasi Hapus'),
content: Text(
'Apakah Anda yakin ingin menghapus stok bantuan "${stok.nama}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
controller.deleteStok(stok.id ?? '');
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Hapus'),
),
],
),
);
}
}

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/profile/controllers/profile_controller.dart';
class ProfileBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ProfileController>(
() => ProfileController(),
);
}
}

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/services/auth_service.dart';
class ProfileController extends GetxController {
final AuthService _authService = Get.find<AuthService>();
final Rx<User?> user = Rx<User?>(null);
final RxBool isLoading = true.obs;
final RxBool isEditing = false.obs;
// Form controllers
late TextEditingController nameController;
late TextEditingController emailController;
late TextEditingController phoneController;
@override
void onInit() {
super.onInit();
nameController = TextEditingController();
emailController = TextEditingController();
phoneController = TextEditingController();
loadUserData();
}
@override
void onClose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
super.onClose();
}
Future<void> loadUserData() async {
isLoading.value = true;
try {
// Mendapatkan data user dari service
final userData = await _authService.getCurrentUser();
user.value = userData;
// Mengisi form controllers dengan data user
if (userData != null) {
nameController.text = userData.name ?? '';
emailController.text = userData.email ?? '';
phoneController.text = userData.phone ?? '';
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal memuat data profil: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
void toggleEditMode() {
isEditing.value = !isEditing.value;
}
Future<void> updateProfile() async {
if (nameController.text.isEmpty) {
Get.snackbar(
'Error',
'Nama tidak boleh kosong',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
try {
// Update user data
final updatedUser = User(
id: user.value?.id,
name: nameController.text,
email: emailController.text,
phone: phoneController.text,
role: user.value?.role,
token: user.value?.token,
);
// Panggil API untuk update profil
await _authService.updateProfile(updatedUser);
// Refresh data
await loadUserData();
// Keluar dari mode edit
isEditing.value = false;
Get.snackbar(
'Sukses',
'Profil berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Error',
'Gagal memperbarui profil: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
Future<void> changePassword(String currentPassword, String newPassword,
String confirmPassword) async {
if (newPassword != confirmPassword) {
Get.snackbar(
'Error',
'Konfirmasi password tidak sesuai',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
try {
// Panggil API untuk ganti password
await _authService.changePassword(currentPassword, newPassword);
Get.back(); // Tutup dialog
Get.snackbar(
'Sukses',
'Password berhasil diubah',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengubah password: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/profile/controllers/profile_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class ProfileView extends GetView<ProfileController> {
const ProfileView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profil'),
actions: [
Obx(() {
if (controller.isEditing.value) {
return IconButton(
icon: const Icon(Icons.save),
onPressed: controller.updateProfile,
);
} else {
return IconButton(
icon: const Icon(Icons.edit),
onPressed: controller.toggleEditMode,
);
}
}),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProfileHeader(),
const SizedBox(height: 24),
_buildProfileForm(),
const SizedBox(height: 24),
_buildPasswordSection(context),
],
),
);
}),
);
}
Widget _buildProfileHeader() {
return Center(
child: Column(
children: [
const CircleAvatar(
radius: 50,
backgroundColor: AppTheme.primaryColor,
child: Icon(
Icons.person,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 16),
Obx(() => Text(
controller.user.value?.name ?? 'Pengguna',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
)),
const SizedBox(height: 4),
Obx(() => Text(
controller.user.value?.role?.toUpperCase() ?? 'PENGGUNA',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
)),
],
),
);
}
Widget _buildProfileForm() {
return Obx(() {
final isEditing = controller.isEditing.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Nama
TextField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
enabled: isEditing,
),
),
const SizedBox(height: 16),
// Email
TextField(
controller: controller.emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
enabled: isEditing,
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
// Nomor Telepon
TextField(
controller: controller.phoneController,
decoration: InputDecoration(
labelText: 'Nomor Telepon',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
enabled: isEditing,
),
keyboardType: TextInputType.phone,
),
],
);
});
}
Widget _buildPasswordSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Keamanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showChangePasswordDialog(context),
icon: const Icon(Icons.lock),
label: const Text('Ubah Password'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
),
),
],
);
}
void _showChangePasswordDialog(BuildContext context) {
final currentPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
Get.dialog(
AlertDialog(
title: const Text('Ubah Password'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: currentPasswordController,
decoration: const InputDecoration(
labelText: 'Password Saat Ini',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: newPasswordController,
decoration: const InputDecoration(
labelText: 'Password Baru',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
TextField(
controller: confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Konfirmasi Password Baru',
border: OutlineInputBorder(),
),
obscureText: true,
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
controller.changePassword(
currentPasswordController.text,
newPasswordController.text,
confirmPasswordController.text,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
),
child: const Text('Simpan'),
),
],
),
);
}
}

View File

@ -4,7 +4,7 @@ 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 {
const SplashView({Key? key}) : super(key: key); const SplashView({super.key});
@override @override
State<SplashView> createState() => _SplashViewState(); State<SplashView> createState() => _SplashViewState();

View File

@ -10,15 +10,24 @@ import 'package:penyaluran_app/app/modules/petugas_desa/views/konfirmasi_penerim
import 'package:penyaluran_app/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart'; import 'package:penyaluran_app/app/modules/petugas_desa/bindings/penerima_binding.dart';
import 'package:penyaluran_app/app/modules/profile/bindings/profile_binding.dart';
import 'package:penyaluran_app/app/modules/profile/views/profile_view.dart';
import 'package:penyaluran_app/app/modules/splash/bindings/splash_binding.dart';
import 'package:penyaluran_app/app/modules/splash/views/splash_view.dart';
part 'app_routes.dart'; part 'app_routes.dart';
class AppPages { class AppPages {
AppPages._(); AppPages._();
static const initial = Routes.login; static const initial = Routes.splash;
static final routes = [ static final routes = [
GetPage(
name: _Paths.splash,
page: () => const SplashView(),
binding: SplashBinding(),
),
GetPage( GetPage(
name: _Paths.login, name: _Paths.login,
page: () => const LoginView(), page: () => const LoginView(),
@ -54,5 +63,10 @@ class AppPages {
page: () => const PelaksanaanPenyaluranView(), page: () => const PelaksanaanPenyaluranView(),
binding: PetugasDesaBinding(), binding: PetugasDesaBinding(),
), ),
GetPage(
name: _Paths.profile,
page: () => const ProfileView(),
binding: ProfileBinding(),
),
]; ];
} }

View File

@ -15,6 +15,7 @@ abstract class Routes {
static const detailPenerima = _Paths.detailPenerima; static const detailPenerima = _Paths.detailPenerima;
static const konfirmasiPenerima = _Paths.konfirmasiPenerima; static const konfirmasiPenerima = _Paths.konfirmasiPenerima;
static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran; static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran;
static const profile = _Paths.profile;
} }
abstract class _Paths { abstract class _Paths {
@ -32,4 +33,5 @@ abstract class _Paths {
static const detailPenerima = '/daftar-penerima/detail'; static const detailPenerima = '/daftar-penerima/detail';
static const konfirmasiPenerima = '/daftar-penerima/konfirmasi'; static const konfirmasiPenerima = '/daftar-penerima/konfirmasi';
static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran'; static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran';
static const profile = '/profile';
} }

View File

@ -0,0 +1,164 @@
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
class AuthService extends GetxService {
final Dio _dio = Dio();
final FlutterSecureStorage _storage = const FlutterSecureStorage();
final Rx<User?> currentUser = Rx<User?>(null);
// Mendapatkan data user saat ini
Future<User?> getCurrentUser() async {
try {
// Implementasi untuk mendapatkan data user dari API atau local storage
// Contoh implementasi sederhana:
final token = await _storage.read(key: 'token');
if (token == null) return null;
final response = await _dio.get(
'/api/user/profile',
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);
if (response.statusCode == 200) {
final user = User.fromJson(response.data['data']);
currentUser.value = user;
return user;
}
return null;
} catch (e) {
print('Error getting current user: $e');
return null;
}
}
// Update profil user
Future<bool> updateProfile(User user) async {
try {
final token = await _storage.read(key: 'token');
if (token == null) return false;
final response = await _dio.put(
'/api/user/profile',
data: {
'name': user.name,
'email': user.email,
'phone': user.phone,
},
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);
if (response.statusCode == 200) {
// Update current user
currentUser.value = user;
return true;
}
return false;
} catch (e) {
print('Error updating profile: $e');
return false;
}
}
// Ganti password
Future<bool> changePassword(
String currentPassword, String newPassword) async {
try {
final token = await _storage.read(key: 'token');
if (token == null) return false;
final response = await _dio.put(
'/api/user/change-password',
data: {
'current_password': currentPassword,
'new_password': newPassword,
},
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);
return response.statusCode == 200;
} catch (e) {
print('Error changing password: $e');
return false;
}
}
// Login
Future<User?> login(String email, String password) async {
try {
final response = await _dio.post(
'/api/auth/login',
data: {
'email': email,
'password': password,
},
);
if (response.statusCode == 200) {
final user = User.fromJson(response.data['data']);
// Simpan token
if (user.token != null) {
await _storage.write(key: 'token', value: user.token);
}
currentUser.value = user;
return user;
}
return null;
} catch (e) {
print('Error logging in: $e');
return null;
}
}
// Logout
Future<void> logout() async {
try {
final token = await _storage.read(key: 'token');
if (token != null) {
await _dio.post(
'/api/auth/logout',
options: Options(
headers: {
'Authorization': 'Bearer $token',
},
),
);
}
} catch (e) {
print('Error logging out: $e');
} finally {
// Hapus token dan user data
await _storage.delete(key: 'token');
currentUser.value = null;
// Navigasi ke halaman login
Get.offAllNamed('/login');
}
}
// Inisialisasi service
Future<AuthService> init() async {
// Coba mendapatkan user saat ini
await getCurrentUser();
return this;
}
}

View File

@ -6,6 +6,9 @@ class SupabaseService extends GetxService {
late final SupabaseClient client; late final SupabaseClient client;
// Cache untuk profil pengguna
Map<String, dynamic>? _cachedUserProfile;
// Ganti dengan URL dan API key Supabase Anda // Ganti dengan URL dan API key Supabase Anda
static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL', static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL',
defaultValue: 'http://labulabs.net:8000'); defaultValue: 'http://labulabs.net:8000');
@ -42,6 +45,7 @@ class SupabaseService extends GetxService {
// Metode untuk logout // Metode untuk logout
Future<void> signOut() async { Future<void> signOut() async {
_cachedUserProfile = null; // Hapus cache saat logout
await client.auth.signOut(); await client.auth.signOut();
} }
@ -53,15 +57,37 @@ class SupabaseService extends GetxService {
// Metode untuk mendapatkan profil pengguna // Metode untuk mendapatkan profil pengguna
Future<Map<String, dynamic>?> getUserProfile() async { Future<Map<String, dynamic>?> getUserProfile() async {
if (currentUser == null) return null; final user = currentUser;
if (user == null) return null;
try {
// Gunakan cache jika tersedia
if (_cachedUserProfile != null && _cachedUserProfile!['id'] == user.id) {
print('Menggunakan data profil dari cache');
return _cachedUserProfile;
}
final response = await client final response = await client
.from('user_profile') .from('user_profile')
.select() .select('*, desa:desa_id(id, nama, kecamatan, kabupaten, provinsi)')
.eq('id', currentUser!.id) .eq('id', user.id)
.maybeSingle(); .maybeSingle();
print('response: $response');
// Simpan ke cache
_cachedUserProfile = response;
// Log untuk debugging
if (response != null && response['desa'] != null) {
print('Desa data: ${response['desa']}');
print('Desa type: ${response['desa'].runtimeType}');
}
return response; return response;
} catch (e) {
print('Error pada getUserProfile: $e');
return null;
}
} }
// Metode untuk mendapatkan role pengguna // Metode untuk mendapatkan role pengguna
@ -70,64 +96,491 @@ class SupabaseService extends GetxService {
return profile?['role']; return profile?['role'];
} }
// Metode untuk mendapatkan data berdasarkan peran // ==================== PETUGAS DESA METHODS ====================
Future<Map<String, dynamic>?> getRoleSpecificData(String role) async {
if (currentUser == null) return null;
switch (role) { // Dashboard methods
case 'WARGA': Future<int?> getTotalPenerima() async {
return await getWargaByUserId(); try {
case 'PETUGASVERIFIKASI': final response =
return await getPetugasVerifikasiData(); await client.from('warga').select('id').eq('status', 'AKTIF');
case 'PETUGASDESA':
return await getPetugasDesaData(); return response.length;
case 'DONATUR': } catch (e) {
return await getDonaturData(); print('Error getting total penerima: $e');
default:
return null; return null;
} }
} }
// Metode untuk mendapatkan data petugas verifikasi Future<int?> getTotalBantuan() async {
Future<Map<String, dynamic>?> getPetugasVerifikasiData() async { try {
if (currentUser == null) return null; final response = await client.from('stok_bantuan').select('jumlah');
double total = 0;
for (var item in response) {
total += (item['jumlah'] ?? 0);
}
return total.toInt();
} catch (e) {
print('Error getting total bantuan: $e');
return null;
}
}
Future<int?> getTotalPenyaluran() async {
try {
final response = await client
.from('penyaluran_bantuan')
.select('id')
.eq('status', 'SELESAI');
return response.length;
} catch (e) {
print('Error getting total penyaluran: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getNotifikasiBelumDibaca(
String userId) async {
try {
final response = await client
.from('notifikasi')
.select('*')
.eq('user_id', userId)
.eq('dibaca', false)
.order('created_at', ascending: false);
return response;
} catch (e) {
print('Error getting notifikasi belum dibaca: $e');
return null;
}
}
// Jadwal penyaluran methods
Future<List<Map<String, dynamic>>?> getJadwalHariIni() async {
try {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final response = await client final response = await client
.from('xx02_PetugasVerifikasi') .from('penyaluran_bantuan')
.select() .select('*')
.eq('userId', currentUser!.id) .gte('tanggal_penyaluran', today.toIso8601String())
.lt('tanggal_penyaluran', tomorrow.toIso8601String())
.inFilter('status', ['DISETUJUI', 'BERLANGSUNG']);
return response;
} catch (e) {
print('Error getting jadwal hari ini: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getJadwalMendatang() async {
try {
final now = DateTime.now();
final tomorrow =
DateTime(now.year, now.month, now.day).add(const Duration(days: 1));
final response = await client
.from('penyaluran_bantuan')
.select('*')
.gte('tanggal_penyaluran', tomorrow.toIso8601String())
.inFilter('status', ['DISETUJUI', 'DIJADWALKAN']);
return response;
} catch (e) {
print('Error getting jadwal mendatang: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getJadwalSelesai() async {
try {
final response = await client
.from('penyaluran_bantuan')
.select('*')
.eq('status', 'SELESAI')
.order('tanggal_penyaluran', ascending: false)
.limit(10);
return response;
} catch (e) {
print('Error getting jadwal selesai: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getPermintaanPenjadwalan() async {
try {
final response = await client
.from('penyaluran_bantuan')
.select('*')
.eq('status', 'MENUNGGU');
return response;
} catch (e) {
print('Error getting permintaan penjadwalan: $e');
return null;
}
}
Future<void> approveJadwal(String jadwalId) async {
try {
await client.from('penyaluran_bantuan').update({
'status': 'DISETUJUI',
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', jadwalId);
} catch (e) {
print('Error approving jadwal: $e');
throw e.toString();
}
}
Future<void> rejectJadwal(String jadwalId, String alasan) async {
try {
await client.from('penyaluran_bantuan').update({
'status': 'DITOLAK',
'alasan_penolakan': alasan,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', jadwalId);
} catch (e) {
print('Error rejecting jadwal: $e');
throw e.toString();
}
}
Future<void> completeJadwal(String jadwalId) async {
try {
await client.from('penyaluran_bantuan').update({
'status': 'SELESAI',
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', jadwalId);
} catch (e) {
print('Error completing jadwal: $e');
throw e.toString();
}
}
// Stok bantuan methods
Future<List<Map<String, dynamic>>?> getStokBantuan() async {
try {
final response = await client.from('stok_bantuan').select('*');
return response;
} catch (e) {
print('Error getting stok bantuan: $e');
return null;
}
}
Future<Map<String, dynamic>?> getStokStatistics() async {
try {
// Get stok masuk
final masukResponse = await client.from('stok_bantuan').select('jumlah');
double masuk = 0;
for (var item in masukResponse) {
masuk += (item['jumlah'] ?? 0);
}
// Get stok keluar
final keluarResponse =
await client.from('detail_penyaluran').select('jumlah');
double keluar = 0;
for (var item in keluarResponse) {
keluar += (item['jumlah'] ?? 0);
}
return {
'masuk': masuk,
'keluar': keluar,
};
} catch (e) {
print('Error getting stok statistics: $e');
return null;
}
}
Future<List<Map<String, dynamic>>?> getBentukBantuan() async {
try {
final response = await client.from('bentuk_bantuan').select('*');
return response;
} catch (e) {
print('Error getting bentuk bantuan: $e');
return null;
}
}
Future<void> addStok(Map<String, dynamic> stok) async {
try {
await client.from('stok_bantuan').insert(stok);
} catch (e) {
print('Error adding stok: $e');
throw e.toString();
}
}
Future<void> updateStok(String stokId, Map<String, dynamic> stok) async {
try {
await client.from('stok_bantuan').update(stok).eq('id', stokId);
} catch (e) {
print('Error updating stok: $e');
throw e.toString();
}
}
Future<void> deleteStok(String stokId) async {
try {
await client.from('stok_bantuan').delete().eq('id', stokId);
} catch (e) {
print('Error deleting stok: $e');
throw e.toString();
}
}
// Penitipan bantuan methods
Future<List<Map<String, dynamic>>?> getPenitipanBantuan() async {
try {
final response = await client.from('penitipan_bantuan').select('*');
return response;
} catch (e) {
print('Error getting penitipan bantuan: $e');
return null;
}
}
Future<void> verifikasiPenitipan(String penitipanId) async {
try {
await client.from('penitipan_bantuan').update({
'status': 'TERVERIFIKASI',
'tanggal_verifikasi': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', penitipanId);
} catch (e) {
print('Error verifying penitipan: $e');
throw e.toString();
}
}
Future<void> tolakPenitipan(String penitipanId, String alasan) async {
try {
await client.from('penitipan_bantuan').update({
'status': 'DITOLAK',
'alasan_penolakan': alasan,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', penitipanId);
} catch (e) {
print('Error rejecting penitipan: $e');
throw e.toString();
}
}
Future<Map<String, dynamic>?> getDonaturById(String donaturId) async {
try {
final response =
await client.from('donatur').select('*').eq('id', donaturId).single();
return response;
} catch (e) {
print('Error getting donatur by id: $e');
return null;
}
}
// Pengaduan methods
Future<List<Map<String, dynamic>>?> getPengaduan() async {
try {
final response = await client.from('pengaduan').select('*');
return response;
} catch (e) {
print('Error getting pengaduan: $e');
return null;
}
}
Future<void> prosesPengaduan(String pengaduanId) async {
try {
await client.from('pengaduan').update({
'status': 'DIPROSES',
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', pengaduanId);
} catch (e) {
print('Error processing pengaduan: $e');
throw e.toString();
}
}
Future<void> tambahTindakanPengaduan(Map<String, dynamic> tindakan) async {
try {
await client.from('tindakan_pengaduan').insert(tindakan);
} catch (e) {
print('Error adding tindakan pengaduan: $e');
throw e.toString();
}
}
Future<void> updateStatusPengaduan(String pengaduanId, String status) async {
try {
await client.from('pengaduan').update({
'status': status,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', pengaduanId);
} catch (e) {
print('Error updating status pengaduan: $e');
throw e.toString();
}
}
Future<List<Map<String, dynamic>>?> getTindakanPengaduan(
String pengaduanId) async {
try {
final response = await client
.from('tindakan_pengaduan')
.select('*')
.eq('pengaduan_id', pengaduanId)
.order('created_at', ascending: false);
return response;
} catch (e) {
print('Error getting tindakan pengaduan: $e');
return null;
}
}
// Penerima bantuan methods
Future<List<Map<String, dynamic>>?> getPenerimaBantuan() async {
try {
final response = await client.from('warga').select('*');
return response;
} catch (e) {
print('Error getting penerima bantuan: $e');
return null;
}
}
Future<void> tambahPenerima(Map<String, dynamic> penerima) async {
try {
await client.from('warga').insert(penerima);
} catch (e) {
print('Error adding penerima: $e');
throw e.toString();
}
}
Future<void> updatePenerima(
String penerimaId, Map<String, dynamic> penerima) async {
try {
await client.from('warga').update(penerima).eq('id', penerimaId);
} catch (e) {
print('Error updating penerima: $e');
throw e.toString();
}
}
Future<void> updateStatusPenerima(String penerimaId, String status) async {
try {
await client.from('warga').update({
'status': status,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', penerimaId);
} catch (e) {
print('Error updating status penerima: $e');
throw e.toString();
}
}
// Laporan methods
Future<List<Map<String, dynamic>>?> getLaporan(
DateTime? tanggalMulai, DateTime? tanggalSelesai) async {
try {
var query = client.from('laporan').select('*');
if (tanggalMulai != null) {
query = query.gte('created_at', tanggalMulai.toIso8601String());
}
if (tanggalSelesai != null) {
query = query.lte('created_at', tanggalSelesai.toIso8601String());
}
final response = await query.order('created_at', ascending: false);
return response;
} catch (e) {
print('Error getting laporan: $e');
return null;
}
}
Future<String?> generateLaporan(Map<String, dynamic> laporan) async {
try {
final response = await client.from('laporan').insert(laporan);
return response[0]['id'];
} catch (e) {
print('Error generating laporan: $e');
throw e.toString();
}
}
Future<String?> downloadLaporan(String laporanId) async {
try {
final response = await client
.from('laporan')
.select('file_urls')
.eq('id', laporanId)
.single();
final fileUrls = response['file_urls'];
if (fileUrls != null && fileUrls.isNotEmpty) {
return fileUrls[0];
}
return null;
} catch (e) {
print('Error downloading laporan: $e');
return null;
}
}
Future<void> deleteLaporan(String laporanId) async {
try {
await client.from('laporan').delete().eq('id', laporanId);
} catch (e) {
print('Error deleting laporan: $e');
throw e.toString();
}
}
// Metode untuk mendapatkan data warga berdasarkan user ID
Future<Map<String, dynamic>?> getWargaByUserId() async {
try {
final user = currentUser;
if (user == null) return null;
final response = await client
.from('warga')
.select('*')
.eq('user_id', user.id)
.maybeSingle(); .maybeSingle();
return response; return response;
} catch (e) {
print('Error getting warga data: $e');
return null;
}
} }
// Metode untuk mendapatkan data petugas desa // Metode untuk membuat profil warga
Future<Map<String, dynamic>?> getPetugasDesaData() async {
if (currentUser == null) return null;
final response = await client
.from('xx01_PetugasDesa')
.select()
.eq('userId', currentUser!.id)
.maybeSingle();
return response;
}
// Metode untuk mendapatkan data donatur
Future<Map<String, dynamic>?> getDonaturData() async {
if (currentUser == null) return null;
final response = await client
.from('xx01_Donatur')
.select()
.eq('userId', currentUser!.id)
.maybeSingle();
return response;
}
// Metode untuk membuat data warga
Future<void> createWargaProfile({ Future<void> createWargaProfile({
required String nik, required String nik,
required String namaLengkap, required String namaLengkap,
@ -138,131 +591,75 @@ class SupabaseService extends GetxService {
DateTime? tanggalLahir, DateTime? tanggalLahir,
String? agama, String? agama,
}) async { }) async {
if (currentUser == null) return; try {
final user = currentUser;
if (user == null) throw 'User tidak ditemukan';
await client.from('xx02_Warga').insert({ await client.from('warga').insert({
'NIK': nik, 'user_id': user.id,
'namaLengkap': namaLengkap, 'nik': nik,
'jenisKelamin': jenisKelamin, 'nama_lengkap': namaLengkap,
'noHp': noHp, 'jenis_kelamin': jenisKelamin,
'no_hp': noHp,
'alamat': alamat, 'alamat': alamat,
'tempatLahir': tempatLahir, 'tempat_lahir': tempatLahir,
'tanggalLahir': tanggalLahir?.toIso8601String(), 'tanggal_lahir': tanggalLahir?.toIso8601String(),
'agama': agama, 'agama': agama,
'userId': currentUser!.id, 'status': 'MENUNGGU_VERIFIKASI',
'email': currentUser!.email, 'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
}); });
// Update user profile role
await client.from('user_profile').upsert({
'id': user.id,
'role': 'WARGA',
'updated_at': DateTime.now().toIso8601String(),
});
} catch (e) {
print('Error creating warga profile: $e');
throw e.toString();
} }
// Metode untuk mendapatkan data warga berdasarkan userId
Future<Map<String, dynamic>?> getWargaByUserId() async {
if (currentUser == null) return null;
final response = await client
.from('xx02_Warga')
.select()
.eq('userId', currentUser!.id)
.maybeSingle();
return response;
} }
// Metode untuk mendapatkan notifikasi pengguna // Metode untuk mendapatkan notifikasi pengguna
Future<List<Map<String, dynamic>>> getUserNotifications( Future<List<Map<String, dynamic>>> getUserNotifications(
{bool unreadOnly = false}) async { {bool unreadOnly = false}) async {
if (currentUser == null) return []; try {
final user = currentUser;
if (user == null) return [];
final query = client.from('Notification').select(); final query = unreadOnly
? client
.from('notifikasi')
.select('*')
.eq('user_id', user.id)
.eq('dibaca', false)
.order('created_at', ascending: false)
: client
.from('notifikasi')
.select('*')
.eq('user_id', user.id)
.order('created_at', ascending: false);
// Tambahkan filter untuk user ID final response = await query;
final filteredQuery = query.eq('userId', currentUser!.id); return response;
} catch (e) {
// Tambahkan filter untuk notifikasi yang belum dibaca jika diperlukan print('Error getting user notifications: $e');
final finalQuery = return [];
unreadOnly ? filteredQuery.eq('isRead', false) : filteredQuery; }
// Tambahkan pengurutan
final response = await finalQuery.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
} }
// Metode untuk menandai notifikasi sebagai telah dibaca // Metode untuk menandai notifikasi sebagai telah dibaca
Future<void> markNotificationAsRead(int notificationId) async { Future<void> markNotificationAsRead(int notificationId) async {
await client try {
.from('Notification') await client.from('notifikasi').update({
.update({'isRead': true}).eq('notificationId', notificationId); 'dibaca': true,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', notificationId);
} catch (e) {
print('Error marking notification as read: $e');
throw e.toString();
} }
// Metode untuk mendapatkan data verifikasi warga
Future<List<Map<String, dynamic>>> getVerifikasiDataWarga() async {
if (currentUser == null) return [];
final response = await client
.from('xx02_VerifikasiDataWarga')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
// Metode untuk mendapatkan data pengajuan bantuan
Future<List<Map<String, dynamic>>> getPengajuanBantuan() async {
if (currentUser == null) return [];
final response = await client
.from('xx02_PengajuanKelayakanBantuan')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
// Metode untuk mendapatkan data skema bantuan
Future<List<Map<String, dynamic>>> getSkemaBantuan() async {
if (currentUser == null) return [];
final response = await client
.from('xx02_SkemaBantuan')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
// Metode untuk mendapatkan data penyaluran bantuan
Future<List<Map<String, dynamic>>> getPenyaluranBantuan() async {
if (currentUser == null) return [];
final response = await client
.from('xx01_PenyaluranBantuan')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
// Metode untuk mendapatkan data penitipan bantuan
Future<List<Map<String, dynamic>>> getPenitipanBantuan() async {
if (currentUser == null) return [];
final response = await client
.from('xx01_PenitipanBantuan')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
// Metode untuk mendapatkan data pengaduan
Future<List<Map<String, dynamic>>> getPengaduan() async {
if (currentUser == null) return [];
final response = await client
.from('xx01_Pengaduan')
.select()
.order('CREATED_AT', ascending: false);
return List<Map<String, dynamic>>.from(response);
} }
} }

View File

@ -0,0 +1,27 @@
import 'package:intl/intl.dart';
class DateFormatter {
static String formatDate(DateTime? date,
{String format = 'dd MMMM yyyy',
String locale = 'id_ID',
String defaultValue = '-'}) {
if (date == null) return defaultValue;
try {
return DateFormat(format, locale).format(date);
} catch (e) {
print('Error formatting date: $e');
return date.toString().split(' ')[0]; // Fallback to basic format
}
}
static String formatNumber(num? number,
{String locale = 'id_ID', String defaultValue = '0'}) {
if (number == null) return defaultValue;
try {
return NumberFormat("#,##0.##", locale).format(number);
} catch (e) {
print('Error formatting number: $e');
return number.toString(); // Fallback to basic format
}
}
}

View File

@ -8,14 +8,13 @@ class NavigationButton extends StatelessWidget {
final VoidCallback onPressed; final VoidCallback onPressed;
const NavigationButton({ const NavigationButton({
Key? key, super.key,
required this.label, required this.label,
this.icon, this.icon,
this.iconWidget, this.iconWidget,
required this.onPressed, required this.onPressed,
}) : assert(icon != null || iconWidget != null, }) : assert(icon != null || iconWidget != null,
'Either icon or iconWidget must be provided'), 'Either icon or iconWidget must be provided');
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -8,12 +8,12 @@ class StatisticCard extends StatelessWidget {
final double height; final double height;
const StatisticCard({ const StatisticCard({
Key? key, super.key,
required this.title, required this.title,
required this.count, required this.count,
required this.subtitle, required this.subtitle,
required this.height, required this.height,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,12 +1,19 @@
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/routes/app_pages.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/services/auth_service.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi data locale untuk format tanggal
await initializeDateFormatting('id_ID', null);
// Inisialisasi Supabase // Inisialisasi Supabase
await initServices(); await initServices();
@ -16,6 +23,10 @@ void main() async {
// Inisialisasi service // Inisialisasi service
Future<void> initServices() async { Future<void> initServices() async {
await Get.putAsync(() => SupabaseService().init()); await Get.putAsync(() => SupabaseService().init());
await Get.putAsync(() => AuthService().init());
// Inisialisasi AuthController secara global
Get.put(AuthController(), permanent: true);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {

View File

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.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) 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);
g_autoptr(FlPluginRegistrar) gtk_registrar = g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar); gtk_plugin_register_with_registrar(gtk_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
gtk gtk
url_launcher_linux url_launcher_linux
) )

View File

@ -6,12 +6,14 @@ import FlutterMacOS
import Foundation import Foundation
import app_links import app_links
import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos 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"))
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"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@ -97,6 +97,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dio:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -134,6 +150,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705
url: "https://pub.dev"
source: hosted
version: "1.2.2"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_spinkit: flutter_spinkit:
dependency: "direct main" dependency: "direct main"
description: description:
@ -232,6 +296,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
jwt_decode: jwt_decode:
dependency: transitive dependency: transitive
description: description:
@ -701,6 +773,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
win32:
dependency: transitive
description:
name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
source: hosted
version: "5.10.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -55,6 +55,12 @@ dependencies:
# Untuk format tanggal dalam bahasa Indonesia # Untuk format tanggal dalam bahasa Indonesia
intl: ^0.19.0 intl: ^0.19.0
# HTTP client
dio: ^5.4.1
# Secure storage
flutter_secure_storage: ^9.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@ -7,11 +7,14 @@
#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 <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"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
flutter_secure_storage_windows
url_launcher_windows url_launcher_windows
) )