diff --git a/lib/app/data/models/bentuk_bantuan_model.dart b/lib/app/data/models/bentuk_bantuan_model.dart index e4a5766..943579a 100644 --- a/lib/app/data/models/bentuk_bantuan_model.dart +++ b/lib/app/data/models/bentuk_bantuan_model.dart @@ -1,19 +1,21 @@ import 'dart:convert'; class BentukBantuanModel { - final String id; - final String nama; + final String? id; + final String? nama; final String? deskripsi; final String? kategori; - final DateTime createdAt; + final String? satuan; + final DateTime? createdAt; final DateTime? updatedAt; BentukBantuanModel({ - required this.id, - required this.nama, + this.id, + this.nama, this.deskripsi, this.kategori, - required this.createdAt, + this.satuan, + this.createdAt, this.updatedAt, }); @@ -28,10 +30,13 @@ class BentukBantuanModel { nama: json["nama"], deskripsi: json["deskripsi"], kategori: json["kategori"], - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + satuan: json["satuan"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { @@ -39,7 +44,8 @@ class BentukBantuanModel { "nama": nama, "deskripsi": deskripsi, "kategori": kategori, - "created_at": createdAt.toIso8601String(), + "satuan": satuan, + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/desa_model.dart b/lib/app/data/models/desa_model.dart new file mode 100644 index 0000000..ec15ed0 --- /dev/null +++ b/lib/app/data/models/desa_model.dart @@ -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 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 toJson() => { + "id": id, + "nama": nama, + "kecamatan": kecamatan, + "kabupaten": kabupaten, + "provinsi": provinsi, + "created_at": createdAt?.toIso8601String(), + "updated_at": updatedAt?.toIso8601String(), + }; +} diff --git a/lib/app/data/models/donatur_model.dart b/lib/app/data/models/donatur_model.dart index f220eb7..5c56774 100644 --- a/lib/app/data/models/donatur_model.dart +++ b/lib/app/data/models/donatur_model.dart @@ -1,23 +1,27 @@ import 'dart:convert'; class DonaturModel { - final String id; - final String nama; + final String? id; + final String? nama; final String? alamat; - final String? noTelp; + final String? telepon; final String? email; - final String? jenisDonatur; // Individu, Organisasi, Perusahaan, dll - final DateTime createdAt; + final String? jenis; + final String? deskripsi; + final String? status; + final DateTime? createdAt; final DateTime? updatedAt; DonaturModel({ - required this.id, - required this.nama, + this.id, + this.nama, this.alamat, - this.noTelp, + this.telepon, this.email, - this.jenisDonatur, - required this.createdAt, + this.jenis, + this.deskripsi, + this.status, + this.createdAt, this.updatedAt, }); @@ -30,23 +34,29 @@ class DonaturModel { id: json["id"], nama: json["nama"], alamat: json["alamat"], - noTelp: json["no_telp"], + telepon: json["telepon"], email: json["email"], - jenisDonatur: json["jenis_donatur"], - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + jenis: json["jenis"], + deskripsi: json["deskripsi"], + status: json["status"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, "nama": nama, "alamat": alamat, - "no_telp": noTelp, + "telepon": telepon, "email": email, - "jenis_donatur": jenisDonatur, - "created_at": createdAt.toIso8601String(), + "jenis": jenis, + "deskripsi": deskripsi, + "status": status, + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/laporan_model.dart b/lib/app/data/models/laporan_model.dart index 8296b60..e250f65 100644 --- a/lib/app/data/models/laporan_model.dart +++ b/lib/app/data/models/laporan_model.dart @@ -1,34 +1,34 @@ import 'dart:convert'; class LaporanModel { - final String id; - final String judul; + final String? id; + final String? judul; final String? deskripsi; - final String jenis; // Contoh: 'penyaluran', 'penitipan', 'pengaduan' + final String? jenis; // Contoh: 'PENYALURAN', 'STOK_BANTUAN', 'PENERIMA' final String? referensiId; // ID dari entitas yang dilaporkan (penyaluran, penitipan, dll) - final String status; // Contoh: 'draft', 'final', 'disetujui' - final String? userId; // Pengguna yang membuat laporan + final String? status; // Contoh: 'draft', 'final', 'disetujui' + final String? petugasId; // Pengguna yang membuat laporan final List? fileUrls; // URL file laporan (PDF, Excel, dll) - final DateTime periodeAwal; - final DateTime periodeAkhir; - final DateTime tanggalLaporan; - final DateTime createdAt; + final DateTime? tanggalMulai; + final DateTime? tanggalSelesai; + final DateTime? tanggalLaporan; + final DateTime? createdAt; final DateTime? updatedAt; LaporanModel({ - required this.id, - required this.judul, + this.id, + this.judul, this.deskripsi, - required this.jenis, + this.jenis, this.referensiId, - required this.status, - this.userId, + this.status, + this.petugasId, this.fileUrls, - required this.periodeAwal, - required this.periodeAkhir, - required this.tanggalLaporan, - required this.createdAt, + this.tanggalMulai, + this.tanggalSelesai, + this.tanggalLaporan, + this.createdAt, this.updatedAt, }); @@ -44,17 +44,29 @@ class LaporanModel { jenis: json["jenis"], referensiId: json["referensi_id"], status: json["status"], - userId: json["user_id"], + petugasId: json["petugas_id"] ?? json["user_id"], fileUrls: json["file_urls"] == null ? null : List.from(json["file_urls"].map((x) => x)), - periodeAwal: DateTime.parse(json["periode_awal"]), - periodeAkhir: DateTime.parse(json["periode_akhir"]), - tanggalLaporan: DateTime.parse(json["tanggal_laporan"]), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + tanggalMulai: json["tanggal_mulai"] != null + ? DateTime.parse(json["tanggal_mulai"]) + : json["periode_awal"] != null + ? DateTime.parse(json["periode_awal"]) + : null, + tanggalSelesai: json["tanggal_selesai"] != null + ? DateTime.parse(json["tanggal_selesai"]) + : json["periode_akhir"] != null + ? DateTime.parse(json["periode_akhir"]) + : null, + tanggalLaporan: json["tanggal_laporan"] != null + ? DateTime.parse(json["tanggal_laporan"]) + : null, + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { @@ -64,14 +76,14 @@ class LaporanModel { "jenis": jenis, "referensi_id": referensiId, "status": status, - "user_id": userId, + "petugas_id": petugasId, "file_urls": fileUrls == null ? null : List.from(fileUrls!.map((x) => x)), - "periode_awal": periodeAwal.toIso8601String(), - "periode_akhir": periodeAkhir.toIso8601String(), - "tanggal_laporan": tanggalLaporan.toIso8601String(), - "created_at": createdAt.toIso8601String(), + "tanggal_mulai": tanggalMulai?.toIso8601String(), + "tanggal_selesai": tanggalSelesai?.toIso8601String(), + "tanggal_laporan": tanggalLaporan?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/lokasi_penyaluran_model.dart b/lib/app/data/models/lokasi_penyaluran_model.dart index 7b40ec6..ceaaeef 100644 --- a/lib/app/data/models/lokasi_penyaluran_model.dart +++ b/lib/app/data/models/lokasi_penyaluran_model.dart @@ -46,9 +46,8 @@ class LokasiPenyaluranModel { kabupaten: json["kabupaten"], provinsi: json["provinsi"], kodePos: json["kode_pos"], - latitude: json["latitude"] != null ? json["latitude"].toDouble() : null, - longitude: - json["longitude"] != null ? json["longitude"].toDouble() : null, + latitude: json["latitude"]?.toDouble(), + longitude: json["longitude"]?.toDouble(), petugasDesaId: json["petugas_desa_id"], createdAt: DateTime.parse(json["created_at"]), updatedAt: json["updated_at"] == null diff --git a/lib/app/data/models/notifikasi_model.dart b/lib/app/data/models/notifikasi_model.dart index 2e5bf30..471a788 100644 --- a/lib/app/data/models/notifikasi_model.dart +++ b/lib/app/data/models/notifikasi_model.dart @@ -1,25 +1,27 @@ import 'dart:convert'; class NotifikasiModel { - final String id; - final String judul; - final String pesan; - final String? jenis; // Contoh: 'penyaluran', 'penitipan', 'pengaduan' - final String? referensiId; // ID dari entitas yang terkait notifikasi - final String? userId; // Pengguna yang menerima notifikasi - final bool dibaca; - final DateTime createdAt; + final String? id; + final String? userId; + final String? judul; + final String? pesan; + final String? jenis; + final String? referensiId; + final bool? dibaca; + final DateTime? tanggalNotifikasi; + final DateTime? createdAt; final DateTime? updatedAt; NotifikasiModel({ - required this.id, - required this.judul, - required this.pesan, + this.id, + this.userId, + this.judul, + this.pesan, this.jenis, this.referensiId, - this.userId, - this.dibaca = false, - required this.createdAt, + this.dibaca, + this.tanggalNotifikasi, + this.createdAt, this.updatedAt, }); @@ -31,27 +33,33 @@ class NotifikasiModel { factory NotifikasiModel.fromJson(Map json) => NotifikasiModel( id: json["id"], + userId: json["user_id"], judul: json["judul"], pesan: json["pesan"], jenis: json["jenis"], referensiId: json["referensi_id"], - userId: json["user_id"], - dibaca: json["dibaca"] ?? false, - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + dibaca: json["dibaca"], + tanggalNotifikasi: json["tanggal_notifikasi"] != null + ? DateTime.parse(json["tanggal_notifikasi"]) + : null, + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, + "user_id": userId, "judul": judul, "pesan": pesan, "jenis": jenis, "referensi_id": referensiId, - "user_id": userId, "dibaca": dibaca, - "created_at": createdAt.toIso8601String(), + "tanggal_notifikasi": tanggalNotifikasi?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/pengaduan_model.dart b/lib/app/data/models/pengaduan_model.dart index 738baf8..924c58e 100644 --- a/lib/app/data/models/pengaduan_model.dart +++ b/lib/app/data/models/pengaduan_model.dart @@ -1,25 +1,29 @@ import 'dart:convert'; class PengaduanModel { - final String id; - final String judul; - final String deskripsi; - final String? userId; // Pengguna yang membuat pengaduan - final String? penyaluranBantuanId; // Referensi ke PenyaluranBantuan - final String status; // Contoh: 'pending', 'diproses', 'selesai' - final List? gambarUrls; // URL gambar bukti pengaduan - final DateTime createdAt; + final String? id; + final String? judul; + final String? deskripsi; + final String? status; + final String? kategori; + final String? pelapor; + final String? kontakPelapor; + final List? gambarUrls; + final DateTime? tanggalPengaduan; + final DateTime? createdAt; final DateTime? updatedAt; PengaduanModel({ - required this.id, - required this.judul, - required this.deskripsi, - this.userId, - this.penyaluranBantuanId, - required this.status, + this.id, + this.judul, + this.deskripsi, + this.status, + this.kategori, + this.pelapor, + this.kontakPelapor, this.gambarUrls, - required this.createdAt, + this.tanggalPengaduan, + this.createdAt, this.updatedAt, }); @@ -32,29 +36,37 @@ class PengaduanModel { id: json["id"], judul: json["judul"], deskripsi: json["deskripsi"], - userId: json["user_id"], - penyaluranBantuanId: json["penyaluran_bantuan_id"], status: json["status"], + kategori: json["kategori"], + pelapor: json["pelapor"], + kontakPelapor: json["kontak_pelapor"], gambarUrls: json["gambar_urls"] == null ? null : List.from(json["gambar_urls"].map((x) => x)), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + tanggalPengaduan: json["tanggal_pengaduan"] != null + ? DateTime.parse(json["tanggal_pengaduan"]) + : null, + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, "judul": judul, "deskripsi": deskripsi, - "user_id": userId, - "penyaluran_bantuan_id": penyaluranBantuanId, "status": status, + "kategori": kategori, + "pelapor": pelapor, + "kontak_pelapor": kontakPelapor, "gambar_urls": gambarUrls == null ? null : List.from(gambarUrls!.map((x) => x)), - "created_at": createdAt.toIso8601String(), + "tanggal_pengaduan": tanggalPengaduan?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/penitipan_bantuan_model.dart b/lib/app/data/models/penitipan_bantuan_model.dart index dd3ae67..7e05371 100644 --- a/lib/app/data/models/penitipan_bantuan_model.dart +++ b/lib/app/data/models/penitipan_bantuan_model.dart @@ -1,31 +1,35 @@ import 'dart:convert'; class PenitipanBantuanModel { - final String id; - final String? donaturId; // Referensi ke Donatur - final String? bentukBantuanId; // Referensi ke BentukBantuan - final String? sumberBantuanId; // Referensi ke SumberBantuan - final double jumlah; - final String? satuan; // Contoh: kg, buah, paket, dll + final String? id; + final String? donaturId; + final String? bentukBantuanId; + final String? nama; + final double? jumlah; + final String? satuan; final String? deskripsi; - final String status; // Contoh: 'diterima', 'disalurkan', 'ditolak' - final List? gambarUrls; // URL gambar bukti penitipan - final DateTime tanggalPenitipan; - final DateTime createdAt; + final String? status; + final String? alasanPenolakan; + final List? gambarUrls; + final DateTime? tanggalPenitipan; + final DateTime? tanggalVerifikasi; + final DateTime? createdAt; final DateTime? updatedAt; PenitipanBantuanModel({ - required this.id, + this.id, this.donaturId, this.bentukBantuanId, - this.sumberBantuanId, - required this.jumlah, + this.nama, + this.jumlah, this.satuan, this.deskripsi, - required this.status, + this.status, + this.alasanPenolakan, this.gambarUrls, - required this.tanggalPenitipan, - required this.createdAt, + this.tanggalPenitipan, + this.tanggalVerifikasi, + this.createdAt, this.updatedAt, }); @@ -39,35 +43,45 @@ class PenitipanBantuanModel { id: json["id"], donaturId: json["donatur_id"], bentukBantuanId: json["bentuk_bantuan_id"], - sumberBantuanId: json["sumber_bantuan_id"], - jumlah: json["jumlah"].toDouble(), + nama: json["nama"], + jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, satuan: json["satuan"], deskripsi: json["deskripsi"], status: json["status"], + alasanPenolakan: json["alasan_penolakan"], gambarUrls: json["gambar_urls"] == null ? null : List.from(json["gambar_urls"].map((x) => x)), - tanggalPenitipan: DateTime.parse(json["tanggal_penitipan"]), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + tanggalPenitipan: json["tanggal_penitipan"] != null + ? DateTime.parse(json["tanggal_penitipan"]) + : null, + tanggalVerifikasi: json["tanggal_verifikasi"] != null + ? 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 toJson() => { "id": id, "donatur_id": donaturId, "bentuk_bantuan_id": bentukBantuanId, - "sumber_bantuan_id": sumberBantuanId, + "nama": nama, "jumlah": jumlah, "satuan": satuan, "deskripsi": deskripsi, "status": status, + "alasan_penolakan": alasanPenolakan, "gambar_urls": gambarUrls == null ? null : List.from(gambarUrls!.map((x) => x)), - "tanggal_penitipan": tanggalPenitipan.toIso8601String(), - "created_at": createdAt.toIso8601String(), + "tanggal_penitipan": tanggalPenitipan?.toIso8601String(), + "tanggal_verifikasi": tanggalVerifikasi?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/penyaluran_bantuan_model.dart b/lib/app/data/models/penyaluran_bantuan_model.dart index f101c34..7f18caf 100644 --- a/lib/app/data/models/penyaluran_bantuan_model.dart +++ b/lib/app/data/models/penyaluran_bantuan_model.dart @@ -1,31 +1,29 @@ import 'dart:convert'; class PenyaluranBantuanModel { - final String id; - final String? penitipanBantuanId; // Referensi ke PenitipanBantuan - 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? id; + final String? judul; final String? deskripsi; - final String status; // Contoh: 'diproses', 'disalurkan', 'dibatalkan' - final List? gambarUrls; // URL gambar bukti penyaluran - final DateTime tanggalPenyaluran; - final DateTime createdAt; + final String? lokasiPenyaluranId; + final String? petugasId; + final String? status; + final String? alasanPenolakan; + final DateTime? tanggalPenjadwalan; + final DateTime? tanggalPenyaluran; + final DateTime? createdAt; final DateTime? updatedAt; PenyaluranBantuanModel({ - required this.id, - this.penitipanBantuanId, - this.lokasiPenyaluranId, - this.petugasDesaId, - required this.jumlah, - this.satuan, + this.id, + this.judul, this.deskripsi, - required this.status, - this.gambarUrls, - required this.tanggalPenyaluran, - required this.createdAt, + this.lokasiPenyaluranId, + this.petugasId, + this.status, + this.alasanPenolakan, + this.tanggalPenjadwalan, + this.tanggalPenyaluran, + this.createdAt, this.updatedAt, }); @@ -37,37 +35,37 @@ class PenyaluranBantuanModel { factory PenyaluranBantuanModel.fromJson(Map json) => PenyaluranBantuanModel( id: json["id"], - penitipanBantuanId: json["penitipan_bantuan_id"], - lokasiPenyaluranId: json["lokasi_penyaluran_id"], - petugasDesaId: json["petugas_desa_id"], - jumlah: json["jumlah"].toDouble(), - satuan: json["satuan"], + judul: json["judul"], deskripsi: json["deskripsi"], + lokasiPenyaluranId: json["lokasi_penyaluran_id"], + petugasId: json["petugas_id"], status: json["status"], - gambarUrls: json["gambar_urls"] == null - ? null - : List.from(json["gambar_urls"].map((x) => x)), - tanggalPenyaluran: DateTime.parse(json["tanggal_penyaluran"]), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + alasanPenolakan: json["alasan_penolakan"], + tanggalPenjadwalan: json["tanggal_penjadwalan"] != null + ? DateTime.parse(json["tanggal_penjadwalan"]) + : null, + tanggalPenyaluran: json["tanggal_penyaluran"] != null + ? DateTime.parse(json["tanggal_penyaluran"]) + : null, + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, - "penitipan_bantuan_id": penitipanBantuanId, - "lokasi_penyaluran_id": lokasiPenyaluranId, - "petugas_desa_id": petugasDesaId, - "jumlah": jumlah, - "satuan": satuan, + "judul": judul, "deskripsi": deskripsi, + "lokasi_penyaluran_id": lokasiPenyaluranId, + "petugas_id": petugasId, "status": status, - "gambar_urls": gambarUrls == null - ? null - : List.from(gambarUrls!.map((x) => x)), - "tanggal_penyaluran": tanggalPenyaluran.toIso8601String(), - "created_at": createdAt.toIso8601String(), + "alasan_penolakan": alasanPenolakan, + "tanggal_penjadwalan": tanggalPenjadwalan?.toIso8601String(), + "tanggal_penyaluran": tanggalPenyaluran?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/stok_bantuan_model.dart b/lib/app/data/models/stok_bantuan_model.dart index a324802..0f87e1a 100644 --- a/lib/app/data/models/stok_bantuan_model.dart +++ b/lib/app/data/models/stok_bantuan_model.dart @@ -1,27 +1,31 @@ import 'dart:convert'; class StokBantuanModel { - final String id; - final String bentukBantuanId; // Referensi ke BentukBantuan - final double jumlahMasuk; - final double jumlahKeluar; - final double stokSisa; + final String? id; + final String? nama; + final String? bentukBantuanId; + final String? sumberBantuanId; + final double? jumlah; final String? satuan; - final String? catatan; - final DateTime tanggalUpdate; - final DateTime createdAt; + final String? deskripsi; + final String? status; + final DateTime? tanggalMasuk; + final DateTime? tanggalKadaluarsa; + final DateTime? createdAt; final DateTime? updatedAt; StokBantuanModel({ - required this.id, - required this.bentukBantuanId, - required this.jumlahMasuk, - required this.jumlahKeluar, - required this.stokSisa, + this.id, + this.nama, + this.bentukBantuanId, + this.sumberBantuanId, + this.jumlah, this.satuan, - this.catatan, - required this.tanggalUpdate, - required this.createdAt, + this.deskripsi, + this.status, + this.tanggalMasuk, + this.tanggalKadaluarsa, + this.createdAt, this.updatedAt, }); @@ -33,29 +37,39 @@ class StokBantuanModel { factory StokBantuanModel.fromJson(Map json) => StokBantuanModel( id: json["id"], + nama: json["nama"], bentukBantuanId: json["bentuk_bantuan_id"], - jumlahMasuk: json["jumlah_masuk"].toDouble(), - jumlahKeluar: json["jumlah_keluar"].toDouble(), - stokSisa: json["stok_sisa"].toDouble(), + sumberBantuanId: json["sumber_bantuan_id"], + jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, satuan: json["satuan"], - catatan: json["catatan"], - tanggalUpdate: DateTime.parse(json["tanggal_update"]), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + deskripsi: json["deskripsi"], + status: json["status"], + tanggalMasuk: json["tanggal_masuk"] != null + ? DateTime.parse(json["tanggal_masuk"]) + : null, + 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 toJson() => { "id": id, + "nama": nama, "bentuk_bantuan_id": bentukBantuanId, - "jumlah_masuk": jumlahMasuk, - "jumlah_keluar": jumlahKeluar, - "stok_sisa": stokSisa, + "sumber_bantuan_id": sumberBantuanId, + "jumlah": jumlah, "satuan": satuan, - "catatan": catatan, - "tanggal_update": tanggalUpdate.toIso8601String(), - "created_at": createdAt.toIso8601String(), + "deskripsi": deskripsi, + "status": status, + "tanggal_masuk": tanggalMasuk?.toIso8601String(), + "tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/tindakan_pengaduan_model.dart b/lib/app/data/models/tindakan_pengaduan_model.dart index e7ceb65..db73099 100644 --- a/lib/app/data/models/tindakan_pengaduan_model.dart +++ b/lib/app/data/models/tindakan_pengaduan_model.dart @@ -1,27 +1,25 @@ import 'dart:convert'; class TindakanPengaduanModel { - final String id; - final String pengaduanId; // Referensi ke Pengaduan - final String? userId; // Pengguna yang melakukan tindakan - final String tindakan; // Deskripsi tindakan yang dilakukan - final String status; // Contoh: 'diproses', 'selesai' + final String? id; + final String? pengaduanId; + final String? tindakan; final String? catatan; - final List? gambarUrls; // URL gambar bukti tindakan - final DateTime tanggalTindakan; - final DateTime createdAt; + final String? status; + final String? petugasId; + final DateTime? tanggalTindakan; + final DateTime? createdAt; final DateTime? updatedAt; TindakanPengaduanModel({ - required this.id, - required this.pengaduanId, - this.userId, - required this.tindakan, - required this.status, + this.id, + this.pengaduanId, + this.tindakan, this.catatan, - this.gambarUrls, - required this.tanggalTindakan, - required this.createdAt, + this.status, + this.petugasId, + this.tanggalTindakan, + this.createdAt, this.updatedAt, }); @@ -34,32 +32,30 @@ class TindakanPengaduanModel { TindakanPengaduanModel( id: json["id"], pengaduanId: json["pengaduan_id"], - userId: json["user_id"], tindakan: json["tindakan"], - status: json["status"], catatan: json["catatan"], - gambarUrls: json["gambar_urls"] == null - ? null - : List.from(json["gambar_urls"].map((x) => x)), - tanggalTindakan: DateTime.parse(json["tanggal_tindakan"]), - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + status: json["status"], + petugasId: json["petugas_id"], + tanggalTindakan: json["tanggal_tindakan"] != null + ? DateTime.parse(json["tanggal_tindakan"]) + : null, + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { "id": id, "pengaduan_id": pengaduanId, - "user_id": userId, "tindakan": tindakan, - "status": status, "catatan": catatan, - "gambar_urls": gambarUrls == null - ? null - : List.from(gambarUrls!.map((x) => x)), - "tanggal_tindakan": tanggalTindakan.toIso8601String(), - "created_at": createdAt.toIso8601String(), + "status": status, + "petugas_id": petugasId, + "tanggal_tindakan": tanggalTindakan?.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/models/user_model.dart b/lib/app/data/models/user_model.dart index f987642..062e157 100644 --- a/lib/app/data/models/user_model.dart +++ b/lib/app/data/models/user_model.dart @@ -1,3 +1,5 @@ +import 'package:penyaluran_app/app/data/models/desa_model.dart'; + class UserModel { final String id; final String email; @@ -5,6 +7,8 @@ class UserModel { final String? avatar; final String role; final bool isActive; + final DesaModel? desa; + final String? desaId; final DateTime? lastLogin; final DateTime? createdAt; final DateTime? updatedAt; @@ -16,27 +20,41 @@ class UserModel { this.avatar, required this.role, this.isActive = true, + this.desa, + this.desaId, this.lastLogin, this.createdAt, this.updatedAt, }); factory UserModel.fromJson(Map 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) { + desaModel = DesaModel.fromJson(json['desa'] as Map); + } + return UserModel( id: json['id'], email: json['email'], name: json['name'], avatar: json['avatar'], + desa: desaModel, + desaId: json['desa_id'], role: json['role'] ?? 'WARGA', isActive: json['is_active'] ?? true, lastLogin: json['last_login'] != null ? DateTime.parse(json['last_login']) : null, - createdAt: json['CREATED_AT'] != null - ? DateTime.parse(json['CREATED_AT']) + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) : null, - updatedAt: json['UPDATED_AT'] != null - ? DateTime.parse(json['UPDATED_AT']) + updatedAt: json['updated_at'] != null + ? DateTime.parse(json['updated_at']) : null, ); } @@ -47,11 +65,13 @@ class UserModel { 'email': email, 'name': name, 'avatar': avatar, + 'desa_id': desaId, + 'desa': desa?.toJson(), 'role': role, 'is_active': isActive, 'last_login': lastLogin?.toIso8601String(), - 'CREATED_AT': createdAt?.toIso8601String(), - 'UPDATED_AT': updatedAt?.toIso8601String(), + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), }; } @@ -60,6 +80,8 @@ class UserModel { String? email, String? name, String? avatar, + DesaModel? desa, + String? desaId, String? role, bool? isActive, DateTime? lastLogin, @@ -71,6 +93,8 @@ class UserModel { email: email ?? this.email, name: name ?? this.name, avatar: avatar ?? this.avatar, + desa: desa ?? this.desa, + desaId: desaId ?? this.desaId, role: role ?? this.role, isActive: isActive ?? this.isActive, 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 json) { + return User( + id: json['id'], + name: json['name'], + email: json['email'], + phone: json['phone'], + role: json['role'], + token: json['token'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'phone': phone, + 'role': role, + 'token': token, + }; + } +} diff --git a/lib/app/data/models/warga_model.dart b/lib/app/data/models/warga_model.dart index f022a3c..2c3393b 100644 --- a/lib/app/data/models/warga_model.dart +++ b/lib/app/data/models/warga_model.dart @@ -2,35 +2,39 @@ import 'dart:convert'; // warga == penerima bantuan class WargaModel { - final String id; - final String nama; + final String? id; + final String? nama; final String? nik; final String? alamat; final String? desa; final String? kecamatan; final String? kabupaten; 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? status; // Contoh: 'aktif', 'nonaktif' + final String? status; // Contoh: 'AKTIF', 'NONAKTIF' final String? lokasiPenyaluranId; // Referensi ke LokasiPenyaluran - final DateTime createdAt; + final DateTime? createdAt; final DateTime? updatedAt; WargaModel({ - required this.id, - required this.nama, + this.id, + this.nama, this.nik, this.alamat, this.desa, this.kecamatan, this.kabupaten, this.provinsi, - this.noTelp, + this.telepon, + this.email, + this.catatan, this.kategori, this.status, this.lokasiPenyaluranId, - required this.createdAt, + this.createdAt, this.updatedAt, }); @@ -48,14 +52,18 @@ class WargaModel { kecamatan: json["kecamatan"], kabupaten: json["kabupaten"], provinsi: json["provinsi"], - noTelp: json["no_telp"], + telepon: json["telepon"] ?? json["no_telp"], + email: json["email"], + catatan: json["catatan"], kategori: json["kategori"], status: json["status"], lokasiPenyaluranId: json["lokasi_penyaluran_id"], - createdAt: DateTime.parse(json["created_at"]), - updatedAt: json["updated_at"] == null - ? null - : DateTime.parse(json["updated_at"]), + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"]) + : null, ); Map toJson() => { @@ -67,11 +75,13 @@ class WargaModel { "kecamatan": kecamatan, "kabupaten": kabupaten, "provinsi": provinsi, - "no_telp": noTelp, + "telepon": telepon, + "email": email, + "catatan": catatan, "kategori": kategori, "status": status, "lokasi_penyaluran_id": lokasiPenyaluranId, - "created_at": createdAt.toIso8601String(), + "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; } diff --git a/lib/app/data/providers/auth_provider.dart b/lib/app/data/providers/auth_provider.dart index 0014fa0..921ca54 100644 --- a/lib/app/data/providers/auth_provider.dart +++ b/lib/app/data/providers/auth_provider.dart @@ -4,65 +4,43 @@ import 'package:penyaluran_app/app/data/models/user_model.dart'; class AuthProvider { final SupabaseService _supabaseService = SupabaseService.to; - // Metode untuk mendaftar pengguna baru - Future signUp(String email, String password) async { - 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; - } - } + // Cache untuk menyimpan data profil pengguna + UserModel? _cachedUser; // Metode untuk login Future signIn(String email, String password) async { try { final response = await _supabaseService.signIn(email, password); - if (response.user != null) { + if (response.user != null && response.user?.email != null) { // Ambil profil pengguna dari database final profileData = await _supabaseService.getUserProfile(); + print('DEBUG: Profile data dari signIn: $profileData'); if (profileData != null) { - return UserModel.fromJson({ + // Buat UserModel dengan data yang ada + _cachedUser = UserModel.fromJson({ ...profileData, 'id': response.user!.id, 'email': response.user!.email!, }); + print( + 'DEBUG: User model dibuat: ${_cachedUser?.name}, desa: ${_cachedUser?.desa?.nama}'); + return _cachedUser; } // Jika profil belum tersedia, gunakan data default - return UserModel( + _cachedUser = UserModel( id: response.user!.id, email: response.user!.email!, role: 'WARGA', // Default role ); + print('DEBUG: User model default dibuat: ${_cachedUser?.email}'); + return _cachedUser; } return null; } catch (e) { + print('Error pada signIn: $e'); rethrow; } } @@ -71,6 +49,7 @@ class AuthProvider { Future signOut() async { try { await _supabaseService.signOut(); + _cachedUser = null; // Hapus cache saat logout } catch (e) { rethrow; } @@ -78,25 +57,50 @@ class AuthProvider { // Metode untuk mendapatkan user saat ini Future 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; if (user != null) { - // Ambil profil pengguna dari database - final profileData = await _supabaseService.getUserProfile(); + try { + // Ambil profil pengguna dari database + final profileData = await _supabaseService.getUserProfile(); + print('DEBUG: Profile data dari getCurrentUser: $profileData'); - if (profileData != null) { - return UserModel.fromJson({ - ...profileData, - 'id': user.id, - 'email': user.email!, - }); + if (profileData != null) { + // Buat UserModel dengan data yang ada + _cachedUser = UserModel.fromJson({ + ...profileData, + 'id': user.id, + 'email': user.email!, + }); + print( + 'DEBUG: User model dibuat: ${_cachedUser?.name}, desa: ${_cachedUser?.desa?.nama}'); + return _cachedUser; + } + + // Jika profil belum tersedia, gunakan data default + _cachedUser = UserModel( + id: user.id, + email: user.email!, + 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; } - - // Jika profil belum tersedia, gunakan data default - return UserModel( - id: user.id, - email: user.email!, - role: 'WARGA', // Default role - ); } return null; } @@ -132,6 +136,9 @@ class AuthProvider { tanggalLahir: tanggalLahir, agama: agama, ); + + // Invalidasi cache setelah membuat profil baru + _cachedUser = null; } // Metode untuk mendapatkan notifikasi pengguna diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart index c2f4d57..073b0f4 100644 --- a/lib/app/modules/auth/controllers/auth_controller.dart +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -15,6 +15,9 @@ class AuthController extends GetxController { final RxBool isLoading = false.obs; final RxBool isWargaProfileComplete = false.obs; + // Flag untuk menandai apakah sudah melakukan pengambilan data profil + final RxBool _hasLoadedProfile = false.obs; + // Form controllers final TextEditingController emailController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); @@ -61,41 +64,76 @@ class AuthController extends GetxController { // Memeriksa status autentikasi Future checkAuthStatus() async { + if (isLoading.value) { + return; // Hindari pemanggilan berulang jika sedang loading + } + isLoading.value = true; 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(); + if (currentUser != null) { + print( + 'User terautentikasi: ${currentUser.email}, role: ${currentUser.role}'); _user.value = currentUser; - - // Periksa apakah profil warga sudah lengkap - 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); - } + _hasLoadedProfile.value = true; + _handleAuthenticatedUser(currentUser); } else { - // Jika tidak ada user yang login, arahkan ke halaman login - if (Get.currentRoute != Routes.login) { - // Bersihkan dependensi form sebelum navigasi - clearFormDependencies(); - Get.offAllNamed(Routes.login); - } + print('Tidak ada user yang terautentikasi'); + _handleUnauthenticatedUser(); } } catch (e) { print('Error checking auth status: $e'); - // Jika terjadi error, arahkan ke halaman login - if (Get.currentRoute != Routes.login) { - // Bersihkan dependensi form sebelum navigasi - clearFormDependencies(); - Get.offAllNamed(Routes.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) { + print('Navigasi ke halaman login'); + // Bersihkan dependensi form sebelum navigasi + clearFormDependencies(); + Get.offAllNamed(Routes.login); + } else { + print('Sudah berada di halaman login'); } } @@ -154,28 +192,58 @@ class AuthController extends GetxController { // Metode untuk login Future 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 final email = emailController.text.trim(); final password = passwordController.text; + print('DEBUG: Email: $email, Password length: ${password.length}'); try { + print('DEBUG: Mengatur isLoading ke true'); isLoading.value = true; + + print('DEBUG: Memanggil _authProvider.signIn'); final user = await _authProvider.signIn( email, password, ); + print('DEBUG: Hasil signIn: ${user != null ? 'Berhasil' : 'Gagal'}'); if (user != null) { + print('DEBUG: User ditemukan, role: ${user.role}'); _user.value = user; + _hasLoadedProfile.value = true; // Tandai bahwa profil sudah diambil clearControllers(); // Arahkan ke dashboard sesuai peran + print('DEBUG: Navigasi berdasarkan peran: ${user.role}'); navigateBasedOnRole(user.role); + } else { + print('DEBUG: User null setelah login berhasil'); } } catch (e) { - print('Error login: $e'); + print('DEBUG: Error detail pada login: $e'); + print('DEBUG: Stack trace: ${StackTrace.current}'); Get.snackbar( 'Error', 'Login gagal: ${e.toString()}', @@ -184,6 +252,7 @@ class AuthController extends GetxController { colorText: Colors.white, ); } finally { + print('DEBUG: Mengatur isLoading ke false'); isLoading.value = false; } } @@ -193,6 +262,7 @@ class AuthController extends GetxController { try { await _authProvider.signOut(); _user.value = null; + _hasLoadedProfile.value = false; // Reset flag saat logout isWargaProfileComplete.value = false; // Bersihkan dependensi form sebelum navigasi diff --git a/lib/app/modules/petugas_desa/bindings/penerima_binding.dart b/lib/app/modules/petugas_desa/bindings/penerima_binding.dart index 73bedcb..ef1bcf7 100644 --- a/lib/app/modules/petugas_desa/bindings/penerima_binding.dart +++ b/lib/app/modules/petugas_desa/bindings/penerima_binding.dart @@ -1,9 +1,15 @@ import 'package:get/get.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 { @override void dependencies() { + // Pastikan AuthController tersedia + if (!Get.isRegistered()) { + Get.put(AuthController(), permanent: true); + } + Get.lazyPut( () => PenerimaController(), fenix: true, diff --git a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart index e840850..b70ad9d 100644 --- a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart +++ b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart @@ -1,16 +1,61 @@ 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/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 { @override void dependencies() { + // Pastikan AuthController tersedia + if (!Get.isRegistered()) { + Get.put(AuthController(), permanent: true); + } + + // Main controller Get.lazyPut( () => PetugasDesaController(), fenix: true, ); - Get.lazyPut( - () => PenerimaController(), + + // Dashboard controller + Get.lazyPut( + () => PetugasDesaDashboardController(), + ); + + // Jadwal penyaluran controller + Get.lazyPut( + () => JadwalPenyaluranController(), + ); + + // Stok bantuan controller + Get.lazyPut( + () => StokBantuanController(), + ); + + // Penitipan bantuan controller + Get.lazyPut( + () => PenitipanBantuanController(), + ); + + // Pengaduan controller + Get.lazyPut( + () => PengaduanController(), + ); + + // Penerima bantuan controller + Get.lazyPut( + () => PenerimaBantuanController(), + ); + + // Laporan controller + Get.lazyPut( + () => LaporanController(), ); } } diff --git a/lib/app/modules/petugas_desa/components/greeting_header.dart b/lib/app/modules/petugas_desa/components/greeting_header.dart index be7268b..c44ebba 100644 --- a/lib/app/modules/petugas_desa/components/greeting_header.dart +++ b/lib/app/modules/petugas_desa/components/greeting_header.dart @@ -43,7 +43,9 @@ class GreetingHeader extends StatelessWidget { ), const SizedBox(height: 5), 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( fontSize: 14, color: Colors.grey[600], diff --git a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart index 6f2b9ac..9c98d28 100644 --- a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart +++ b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart @@ -1,21 +1,21 @@ 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/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; class JadwalSectionWidget extends StatelessWidget { - final PetugasDesaController controller; + final JadwalPenyaluranController controller; final String title; - final List> jadwalList; + final List jadwalList; final String status; const JadwalSectionWidget({ - Key? key, + super.key, required this.controller, required this.title, required this.jadwalList, required this.status, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -62,20 +62,24 @@ class JadwalSectionWidget extends StatelessWidget { ); } - List> _getCurrentJadwalList() { + List _getCurrentJadwalList() { switch (title) { case 'Hari Ini': - return controller.jadwalHariIni; + return controller.jadwalHariIni.toList(); case 'Mendatang': - return controller.jadwalMendatang; + return controller.jadwalMendatang.toList(); case 'Selesai': - return controller.jadwalSelesai; + return controller.jadwalSelesai.toList(); default: return jadwalList; } } - Widget _buildJadwalItem(TextTheme textTheme, Map jadwal) { + Widget _buildJadwalItem(TextTheme textTheme, dynamic jadwal) { + // Konversi jadwal ke Map jika itu adalah PenyaluranBantuanModel + final Map jadwalData = + jadwal is Map ? jadwal : jadwal.toJson(); + Color statusColor; switch (status) { case 'Aktif': @@ -94,7 +98,7 @@ class JadwalSectionWidget extends StatelessWidget { return GestureDetector( onTap: () { // Navigasi ke halaman pelaksanaan penyaluran dengan data jadwal - Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwal); + Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwalData); }, child: Container( width: double.infinity, @@ -120,7 +124,7 @@ class JadwalSectionWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - jadwal['lokasi'] ?? '', + jadwalData['lokasi'] ?? '', style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -144,23 +148,23 @@ class JadwalSectionWidget extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'Jenis Bantuan: ${jadwal['jenis_bantuan'] ?? ''}', + 'Jenis Bantuan: ${jadwalData['jenis_bantuan'] ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), Text( - 'Tanggal: ${jadwal['tanggal'] ?? ''}', + 'Tanggal: ${jadwalData['tanggal'] ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), Text( - 'Waktu: ${jadwal['waktu'] ?? ''}', + 'Waktu: ${jadwalData['waktu'] ?? ''}', style: textTheme.bodyMedium, ), - if (jadwal['jumlah_penerima'] != null) ...[ + if (jadwalData['jumlah_penerima'] != null) ...[ const SizedBox(height: 4), Text( - 'Jumlah Penerima: ${jadwal['jumlah_penerima']}', + 'Jumlah Penerima: ${jadwalData['jumlah_penerima']}', style: textTheme.bodyMedium, ), ], diff --git a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart index 79d6f6c..46d6263 100644 --- a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart +++ b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart @@ -1,16 +1,16 @@ 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/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class PermintaanPenjadwalanSummaryWidget extends StatelessWidget { - final PetugasDesaController controller; + final JadwalPenyaluranController controller; const PermintaanPenjadwalanSummaryWidget({ - Key? key, + super.key, required this.controller, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -134,8 +134,11 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget { }); } - Widget _buildPermintaanPreview( - TextTheme textTheme, Map permintaan) { + Widget _buildPermintaanPreview(TextTheme textTheme, dynamic permintaan) { + // Konversi permintaan ke Map jika itu adalah PenyaluranBantuanModel + final Map permintaanData = + permintaan is Map ? permintaan : permintaan.toJson(); + return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 8), @@ -152,7 +155,7 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget { children: [ Expanded( child: Text( - permintaan['nama'] ?? '', + permintaanData['nama'] ?? '', style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -178,12 +181,12 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget { ), const SizedBox(height: 4), Text( - 'Jenis: ${permintaan['jenis_bantuan'] ?? ''}', + 'Jenis: ${permintaanData['jenis_bantuan'] ?? ''}', style: textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), Text( - 'Tanggal: ${permintaan['tanggal_permintaan'] ?? ''}', + 'Tanggal: ${permintaanData['tanggal_permintaan'] ?? ''}', style: textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), diff --git a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart index 82d7297..f63cd18 100644 --- a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart +++ b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart'; +import 'package:penyaluran_app/app/data/models/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'; class PermintaanPenjadwalanWidget extends StatelessWidget { - final PetugasDesaController controller; + final JadwalPenyaluranController controller; const PermintaanPenjadwalanWidget({ - Key? key, + super.key, required this.controller, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -90,7 +91,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { // Widget untuk menampilkan item permintaan penjadwalan Widget _buildPermintaanItem( - TextTheme textTheme, Map permintaan) { + TextTheme textTheme, PenyaluranBantuanModel permintaan) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 10), @@ -119,7 +120,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - permintaan['nama'] ?? '', + permintaan.judul ?? '', style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -143,22 +144,22 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'NIK: ${permintaan['nik'] ?? ''}', + 'ID: ${permintaan.id ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), Text( - 'Jenis Bantuan: ${permintaan['jenis_bantuan'] ?? ''}', + 'Jenis Bantuan: ${permintaan.judul ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), Text( - 'Tanggal Permintaan: ${permintaan['tanggal_permintaan'] ?? ''}', + 'Tanggal Permintaan: ${permintaan.createdAt?.toString().substring(0, 10) ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), Text( - 'Alamat: ${permintaan['alamat'] ?? ''}', + 'Deskripsi: ${permintaan.deskripsi ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 12), @@ -191,15 +192,15 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { } // Dialog untuk konfirmasi permintaan - void _showKonfirmasiDialog(Map permintaan) { + void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) { String? selectedJadwalId; // Data jadwal yang tersedia dari controller final jadwalOptions = controller.jadwalMendatang.map((jadwal) { return DropdownMenuItem( - value: jadwal['id'], + value: jadwal.id, child: Text( - '${jadwal['tanggal']} - ${jadwal['lokasi']} (${jadwal['jenis_bantuan']})'), + '${jadwal.tanggalPenjadwalan?.toString().substring(0, 10) ?? ''} - ${jadwal.lokasiPenyaluranId ?? ''} (${jadwal.judul ?? ''})'), ); }).toList(); @@ -219,7 +220,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama']}.'), + 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan.judul}.'), const SizedBox(height: 16), const Text('Pilih jadwal penyaluran:'), const SizedBox(height: 8), @@ -245,9 +246,8 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { onPressed: () { if (selectedJadwalId != null) { // Panggil metode konfirmasi di controller - controller.konfirmasiPermintaanPenjadwalan( - permintaan['id'], - selectedJadwalId!, + controller.approveJadwal( + permintaan.id ?? '', ); Get.back(); @@ -279,7 +279,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { } // Dialog untuk menolak permintaan - void _showTolakDialog(Map permintaan) { + void _showTolakDialog(PenyaluranBantuanModel permintaan) { final TextEditingController alasanController = TextEditingController(); Get.dialog( @@ -290,7 +290,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama']}.'), + 'Anda akan menolak permintaan penjadwalan dari ${permintaan.judul}.'), const SizedBox(height: 16), const Text('Alasan penolakan:'), const SizedBox(height: 8), @@ -313,8 +313,8 @@ class PermintaanPenjadwalanWidget extends StatelessWidget { onPressed: () { if (alasanController.text.trim().isNotEmpty) { // Panggil metode tolak di controller - controller.tolakPermintaanPenjadwalan( - permintaan['id'], + controller.rejectJadwal( + permintaan.id ?? '', alasanController.text.trim(), ); diff --git a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart new file mode 100644 index 0000000..cb1f5f2 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -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(); + 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 jadwalHariIni = + [].obs; + final RxList jadwalMendatang = + [].obs; + final RxList jadwalSelesai = + [].obs; + + // Data untuk permintaan penjadwalan + final RxList permintaanPenjadwalan = + [].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 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 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 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 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 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 refreshData() async { + isLoading.value = true; + try { + await loadJadwalData(); + await loadPermintaanPenjadwalanData(); + } finally { + isLoading.value = false; + } + } + + void changeCategory(int index) { + selectedCategoryIndex.value = index; + } +} diff --git a/lib/app/modules/petugas_desa/controllers/laporan_controller.dart b/lib/app/modules/petugas_desa/controllers/laporan_controller.dart new file mode 100644 index 0000000..5e01f84 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/laporan_controller.dart @@ -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(); + 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 daftarLaporan = [].obs; + + // Filter tanggal + final Rx tanggalMulai = Rx(null); + final Rx tanggalSelesai = Rx(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 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 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 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 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 applyFilter() async { + await loadLaporanData(); + } + + Future refreshData() async { + isLoading.value = true; + try { + await loadLaporanData(); + } finally { + isLoading.value = false; + } + } + + void changeCategory(int index) { + selectedCategoryIndex.value = index; + } + + List 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; + } + } +} diff --git a/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart new file mode 100644 index 0000000..03f852f --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart @@ -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(); + 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 daftarPenerima = [].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 penerimaFormKey = GlobalKey(); + + 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 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 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 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 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 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 refreshData() async { + isLoading.value = true; + try { + await loadPenerimaData(); + } finally { + isLoading.value = false; + } + } + + void changeCategory(int index) { + selectedCategoryIndex.value = index; + } + + List 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; + } +} diff --git a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart index d18d898..7b7ae58 100644 --- a/lib/app/modules/petugas_desa/controllers/penerima_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penerima_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:penyaluran_app/app/utils/date_formatter.dart'; class PenerimaController extends GetxController { final RxList> daftarPenerima = diff --git a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart new file mode 100644 index 0000000..800b127 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart @@ -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(); + 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 daftarPengaduan = [].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 tindakanFormKey = GlobalKey(); + + UserModel? get user => _authController.user; + + @override + void onInit() { + super.onInit(); + loadPengaduanData(); + } + + @override + void onClose() { + searchController.dispose(); + tindakanController.dispose(); + catatanController.dispose(); + super.onClose(); + } + + Future 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 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 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 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> 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 refreshData() async { + isLoading.value = true; + try { + await loadPengaduanData(); + } finally { + isLoading.value = false; + } + } + + void changeCategory(int index) { + selectedCategoryIndex.value = index; + } + + List 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; + } +} diff --git a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart new file mode 100644 index 0000000..7c9f4f8 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart @@ -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(); + 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 daftarPenitipan = + [].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 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 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 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 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 refreshData() async { + isLoading.value = true; + try { + await loadPenitipanData(); + } finally { + isLoading.value = false; + } + } + + void changeCategory(int index) { + selectedCategoryIndex.value = index; + } + + List 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; + } + } +} diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart index f9a2950..9beeb16 100644 --- a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.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/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/services/supabase_service.dart'; @@ -8,81 +9,63 @@ class PetugasDesaController extends GetxController { final AuthController _authController = Get.find(); final SupabaseService _supabaseService = SupabaseService.to; - final RxBool isLoading = false.obs; - final Rx?> roleData = Rx?>(null); - - // Indeks kategori yang dipilih untuk filter - final RxInt selectedCategoryIndex = 0.obs; - // Indeks tab yang aktif di bottom navigation bar 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> jadwalHariIni = - >[].obs; - final RxList> jadwalMendatang = - >[].obs; - final RxList> jadwalSelesai = - >[].obs; - - // Data untuk permintaan penjadwalan - final RxList> permintaanPenjadwalan = - >[].obs; - final RxInt jumlahPermintaanPenjadwalan = 0.obs; - - // Data untuk notifikasi - final RxList> notifikasiBelumDibaca = - >[].obs; - final RxInt jumlahNotifikasiBelumDibaca = 0.obs; - - // Data untuk inventaris - final RxList> daftarInventaris = - >[].obs; - final RxDouble totalStok = 0.0.obs; - final RxDouble stokMasuk = 0.0.obs; - final RxDouble stokKeluar = 0.0.obs; - - // Data untuk penitipan - final RxList> daftarPenitipan = - >[].obs; - final RxInt jumlahMenunggu = 0.obs; - final RxInt jumlahTerverifikasi = 0.obs; - final RxInt jumlahDitolak = 0.obs; - - // Data untuk pengaduan - final RxList> daftarPengaduan = - >[].obs; - final RxInt jumlahDiproses = 0.obs; - final RxInt jumlahTindakan = 0.obs; - final RxInt jumlahSelesai = 0.obs; - // Controller untuk pencarian final TextEditingController searchController = TextEditingController(); + // Data profil pengguna dari cache + final RxMap userProfile = RxMap({}); + + // Model desa dari cache + final Rx desaModel = Rx(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 jadwalHariIni = [].obs; + 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 { + // 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 void onInit() { super.onInit(); - - // Inisialisasi manual untuk pengaduan (untuk debugging) - jumlahDiproses.value = 3; - print('onInit - Jumlah pengaduan diproses: ${jumlahDiproses.value}'); - - loadRoleData(); - loadDashboardData(); - loadJadwalData(); - loadPermintaanPenjadwalanData(); + loadUserProfile(); loadNotifikasiData(); - loadInventarisData(); + loadJadwalData(); loadPenitipanData(); loadPengaduanData(); } @@ -93,584 +76,106 @@ class PetugasDesaController extends GetxController { super.onClose(); } - Future loadRoleData() async { - isLoading.value = true; + // Metode untuk memuat data profil pengguna dari cache + Future loadUserProfile() async { try { + // Jika user sudah ada di AuthController, tidak perlu mengambil data lagi if (user != null) { - final data = await _supabaseService.getRoleSpecificData(role); - roleData.value = data; + print('DEBUG: User ditemukan di AuthController: ${user!.email}'); + print('DEBUG: User desa: ${user!.desa?.nama}'); + + // 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) { + try { + final desaData = profileData['desa'] as Map; + print('DEBUG: Desa data ditemukan: $desaData'); + } catch (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'); } } catch (e) { - print('Error loading role data: $e'); - } finally { - isLoading.value = false; - } - } - - Future loadDashboardData() async { - try { - // Simulasi data untuk dashboard - await Future.delayed(const Duration(milliseconds: 800)); - - 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) { - print('Error loading dashboard data: $e'); - } - } - - Future 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) { - print('Error loading jadwal data: $e'); - } - } - - Future 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'); + print('Error loading user profile: $e'); } } + // Metode untuk memuat data notifikasi Future loadNotifikasiData() async { try { - // Simulasi data untuk notifikasi - await Future.delayed(const Duration(milliseconds: 500)); - - // Hitung jumlah notifikasi yang belum dibaca - final List> notifikasi = [ - { - '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; + if (user != null) { + final notifikasiData = + await _supabaseService.getNotifikasiBelumDibaca(user!.id); + if (notifikasiData != null) { + jumlahNotifikasiBelumDibaca.value = notifikasiData.length; + } + } } catch (e) { print('Error loading notifikasi data: $e'); } } - Future loadInventarisData() async { + // Metode untuk memuat data jadwal + Future loadJadwalData() async { try { - // Simulasi data untuk inventaris - await Future.delayed(const Duration(milliseconds: 700)); - - 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; + final jadwalHariIniData = await _supabaseService.getJadwalHariIni(); + if (jadwalHariIniData != null) { + jadwalHariIni.value = jadwalHariIniData; + } } catch (e) { - print('Error loading inventaris data: $e'); + print('Error loading jadwal data: $e'); } } + // Metode untuk memuat data penitipan Future loadPenitipanData() async { try { - // Simulasi data untuk penitipan - await Future.delayed(const Duration(milliseconds: 600)); - - 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; + // Simulasi data untuk contoh + jumlahMenunggu.value = 3; } catch (e) { print('Error loading penitipan data: $e'); } } + // Metode untuk memuat data pengaduan Future loadPengaduanData() async { try { - // Simulasi data untuk pengaduan - await Future.delayed(const Duration(milliseconds: 650)); - - // 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; + // Simulasi data untuk contoh + jumlahDiproses.value = 2; } catch (e) { print('Error loading pengaduan data: $e'); } } - // Method untuk memperbarui jumlah pengaduan secara manual (untuk debugging) - void updatePengaduanCounter() { - jumlahDiproses.value = 5; // Set nilai secara manual - update(); // Perbarui UI - print( - 'Counter pengaduan diperbarui secara manual: ${jumlahDiproses.value}'); - } - - 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 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 memperbarui counter pengaduan + Future updatePengaduanCounter() async { + try { + await loadPengaduanData(); + } catch (e) { + print('Error updating pengaduan counter: $e'); + } } + // Metode untuk mengubah tab aktif void changeTab(int index) { activeTabIndex.value = index; } - // Metode untuk konfirmasi permintaan penjadwalan - Future konfirmasiPermintaanPenjadwalan( - String id, String jadwalId) async { - 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 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; - } + // Metode untuk logout + Future logout() async { + await _authController.logout(); } } diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart new file mode 100644 index 0000000..f261988 --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_dashboard_controller.dart @@ -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(); + final SupabaseService _supabaseService = SupabaseService.to; + + final RxBool isLoading = false.obs; + + // Data profil pengguna dari cache + final RxMap userProfile = RxMap({}); + + // 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 notifikasiBelumDibaca = [].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 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 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 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 refreshData() async { + isLoading.value = true; + try { + await loadDashboardData(); + await loadNotifikasiData(); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart new file mode 100644 index 0000000..108ca2e --- /dev/null +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -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(); + final SupabaseService _supabaseService = SupabaseService.to; + + final RxBool isLoading = false.obs; + + // Data untuk stok bantuan + final RxList daftarStokBantuan = [].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 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 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 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 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 refreshData() async { + isLoading.value = true; + try { + await loadStokBantuanData(); + } finally { + isLoading.value = false; + } + } + + List 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(); + } + } +} diff --git a/lib/app/modules/petugas_desa/views/dashboard_view.dart b/lib/app/modules/petugas_desa/views/dashboard_view.dart index 2433998..320507e 100644 --- a/lib/app/modules/petugas_desa/views/dashboard_view.dart +++ b/lib/app/modules/petugas_desa/views/dashboard_view.dart @@ -22,9 +22,9 @@ class DashboardView extends GetView { children: [ // Header dengan greeting GreetingHeader( - name: controller.roleData.value?['namaLengkap'] ?? 'Ahmad', + name: controller.namaLengkap, role: 'Petugas Desa', - desa: controller.roleData.value?['Desa'] ?? 'Jatihurip', + desa: controller.desa, ), const SizedBox(height: 20), diff --git a/lib/app/modules/petugas_desa/views/inventaris_view.dart b/lib/app/modules/petugas_desa/views/inventaris_view.dart deleted file mode 100644 index 04b6e4b..0000000 --- a/lib/app/modules/petugas_desa/views/inventaris_view.dart +++ /dev/null @@ -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 { - 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> 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 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, - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/app/modules/petugas_desa/views/notifikasi_view.dart b/lib/app/modules/petugas_desa/views/notifikasi_view.dart index ac2b6d8..512d4ee 100644 --- a/lib/app/modules/petugas_desa/views/notifikasi_view.dart +++ b/lib/app/modules/petugas_desa/views/notifikasi_view.dart @@ -118,8 +118,8 @@ class NotifikasiView extends GetView { ); }, backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.done_all), tooltip: 'Tandai Semua Dibaca', + child: const Icon(Icons.done_all), ), ); } diff --git a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart index ef46751..cc7c3f2 100644 --- a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart @@ -237,8 +237,7 @@ class PelaksanaanPenyaluranView extends GetView { // Daftar penerima ...daftarPenerima - .map((penerima) => _buildPenerimaItem(context, penerima)) - .toList(), + .map((penerima) => _buildPenerimaItem(context, penerima)), ], ), ); diff --git a/lib/app/modules/petugas_desa/views/penyaluran_view.dart b/lib/app/modules/petugas_desa/views/penyaluran_view.dart index 31dcfd8..99588c5 100644 --- a/lib/app/modules/petugas_desa/views/penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/penyaluran_view.dart @@ -1,11 +1,11 @@ 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/modules/petugas_desa/controllers/jadwal_penyaluran_controller.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/permintaan_penjadwalan_summary_widget.dart'; -class PenyaluranView extends GetView { +class PenyaluranView extends GetView { const PenyaluranView({super.key}); @override diff --git a/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart b/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart index 867b0db..756dd21 100644 --- a/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart +++ b/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa_controller.dart'; +import 'package:penyaluran_app/app/data/models/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'; -class PermintaanPenjadwalanView extends GetView { +class PermintaanPenjadwalanView extends GetView { const PermintaanPenjadwalanView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { // Pastikan controller sudah diinisialisasi - if (!Get.isRegistered()) { - Get.put(PetugasDesaController()); + if (!Get.isRegistered()) { + Get.put(JadwalPenyaluranController()); } return Scaffold( @@ -236,7 +237,7 @@ class PermintaanPenjadwalanView extends GetView { ); } - Widget _buildPermintaanItem(BuildContext context, Map item) { + Widget _buildPermintaanItem(BuildContext context, PenyaluranBantuanModel item) { Color statusColor = Colors.orange; IconData statusIcon = Icons.pending_actions; @@ -265,7 +266,7 @@ class PermintaanPenjadwalanView extends GetView { children: [ Expanded( child: Text( - item['nama'] ?? '', + item.judul ?? '', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -307,8 +308,8 @@ class PermintaanPenjadwalanView extends GetView { child: _buildItemDetail( context, icon: Icons.person, - label: 'NIK', - value: item['nik'] ?? '', + label: 'ID', + value: item.id ?? '', ), ), Expanded( @@ -316,7 +317,7 @@ class PermintaanPenjadwalanView extends GetView { context, icon: Icons.category, label: 'Jenis Bantuan', - value: item['jenis_bantuan'] ?? '', + value: item.judul ?? '', ), ), ], @@ -329,15 +330,15 @@ class PermintaanPenjadwalanView extends GetView { context, icon: Icons.calendar_today, label: 'Tanggal Permintaan', - value: item['tanggal_permintaan'] ?? '', + value: item.createdAt?.toString().substring(0, 10) ?? '', ), ), Expanded( child: _buildItemDetail( context, icon: Icons.location_on, - label: 'Alamat', - value: item['alamat'] ?? '', + label: 'Deskripsi', + value: item.deskripsi ?? '', ), ), ], @@ -420,15 +421,15 @@ class PermintaanPenjadwalanView extends GetView { } // Dialog untuk konfirmasi permintaan - void _showKonfirmasiDialog(Map permintaan) { + void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) { String? selectedJadwalId; // Data jadwal yang tersedia dari controller final jadwalOptions = controller.jadwalMendatang.map((jadwal) { return DropdownMenuItem( - value: jadwal['id'], + value: jadwal.id, child: Text( - '${jadwal['tanggal'] ?? ''} - ${jadwal['lokasi'] ?? ''} (${jadwal['jenis_bantuan'] ?? ''})'), + '${jadwal.tanggalPenjadwalan?.toString().substring(0, 10) ?? ''} - ${jadwal.lokasiPenyaluranId ?? ''} (${jadwal.judul ?? ''})'), ); }).toList(); @@ -448,7 +449,7 @@ class PermintaanPenjadwalanView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'), + 'Anda akan mengkonfirmasi permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'), const SizedBox(height: 16), const Text('Pilih jadwal penyaluran:'), const SizedBox(height: 8), @@ -474,9 +475,8 @@ class PermintaanPenjadwalanView extends GetView { onPressed: () { if (selectedJadwalId != null) { // Panggil metode konfirmasi di controller - controller.konfirmasiPermintaanPenjadwalan( - permintaan['id'] ?? '', - selectedJadwalId ?? '', + controller.approveJadwal( + permintaan.id ?? '', ); Get.back(); @@ -508,7 +508,7 @@ class PermintaanPenjadwalanView extends GetView { } // Dialog untuk menolak permintaan - void _showTolakDialog(Map permintaan) { + void _showTolakDialog(PenyaluranBantuanModel permintaan) { final TextEditingController alasanController = TextEditingController(); Get.dialog( @@ -519,7 +519,7 @@ class PermintaanPenjadwalanView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Anda akan menolak permintaan penjadwalan dari ${permintaan['nama'] ?? 'Penerima'}.'), + 'Anda akan menolak permintaan penjadwalan dari ${permintaan.judul ?? 'Penerima'}.'), const SizedBox(height: 16), const Text('Alasan penolakan:'), const SizedBox(height: 8), @@ -542,8 +542,8 @@ class PermintaanPenjadwalanView extends GetView { onPressed: () { if (alasanController.text.trim().isNotEmpty) { // Panggil metode tolak di controller - controller.tolakPermintaanPenjadwalan( - permintaan['id'] ?? '', + controller.rejectJadwal( + permintaan.id ?? '', alasanController.text.trim(), ); diff --git a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart index 1db895d..86e29e5 100644 --- a/lib/app/modules/petugas_desa/views/petugas_desa_view.dart +++ b/lib/app/modules/petugas_desa/views/petugas_desa_view.dart @@ -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/penyaluran_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/pengaduan_view.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; @@ -35,7 +35,7 @@ class PetugasDesaView extends GetView { case 3: return const Text('Pengaduan'); case 4: - return const Text('Inventaris'); + return const Text('Stok Bantuan'); default: return const Text('Petugas Desa'); } @@ -94,7 +94,7 @@ class PetugasDesaView extends GetView { ], ); - // Tombol tambah untuk jadwal dan inventaris + // Tombol tambah untuk jadwal dan stok bantuan if (activeTab == 1) { return Row( mainAxisSize: MainAxisSize.min, @@ -115,9 +115,9 @@ class PetugasDesaView extends GetView { children: [ IconButton( icon: const Icon(Icons.add), - tooltip: 'Tambah Inventaris', + tooltip: 'Tambah Stok Bantuan', onPressed: () { - // Implementasi untuk menambah inventaris baru + // Implementasi untuk menambah stok bantuan baru }, ), notificationButton, @@ -171,7 +171,7 @@ class PetugasDesaView extends GetView { case 3: return const PengaduanView(); case 4: - return const InventarisView(); + return const StokBantuanView(); default: return const DashboardView(); } @@ -325,7 +325,7 @@ class PetugasDesaView extends GetView { }), Obx(() => ListTile( leading: const Icon(Icons.inventory_2_outlined), - title: const Text('Inventaris'), + title: const Text('Stok Bantuan'), selected: controller.activeTabIndex.value == 4, selectedColor: AppTheme.primaryColor, onTap: () { @@ -390,6 +390,7 @@ class PetugasDesaView extends GetView { onTap: () { // Navigasi ke halaman profil Navigator.pop(context); + Get.toNamed('/profile'); }, ), ListTile( @@ -623,7 +624,7 @@ class PetugasDesaView extends GetView { const BottomNavigationBarItem( icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), - label: 'Inventaris', + label: 'Stok Bantuan', ), ], ); diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart new file mode 100644 index 0000000..faee4a7 --- /dev/null +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -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 { + 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(); + 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(); + 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'), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/profile/bindings/profile_binding.dart b/lib/app/modules/profile/bindings/profile_binding.dart new file mode 100644 index 0000000..c273b95 --- /dev/null +++ b/lib/app/modules/profile/bindings/profile_binding.dart @@ -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(), + ); + } +} diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart new file mode 100644 index 0000000..fe47fd7 --- /dev/null +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -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(); + + final Rx user = Rx(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 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 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 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; + } + } +} diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart new file mode 100644 index 0000000..fdb046b --- /dev/null +++ b/lib/app/modules/profile/views/profile_view.dart @@ -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 { + 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'), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/splash/views/splash_view.dart b/lib/app/modules/splash/views/splash_view.dart index b1eb1bb..fff53ca 100644 --- a/lib/app/modules/splash/views/splash_view.dart +++ b/lib/app/modules/splash/views/splash_view.dart @@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; class SplashView extends StatefulWidget { - const SplashView({Key? key}) : super(key: key); + const SplashView({super.key}); @override State createState() => _SplashViewState(); diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index ed88b2a..1f33893 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -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/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'; class AppPages { AppPages._(); - static const initial = Routes.login; + static const initial = Routes.splash; static final routes = [ + GetPage( + name: _Paths.splash, + page: () => const SplashView(), + binding: SplashBinding(), + ), GetPage( name: _Paths.login, page: () => const LoginView(), @@ -54,5 +63,10 @@ class AppPages { page: () => const PelaksanaanPenyaluranView(), binding: PetugasDesaBinding(), ), + GetPage( + name: _Paths.profile, + page: () => const ProfileView(), + binding: ProfileBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 9bba31c..35e2f72 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -15,6 +15,7 @@ abstract class Routes { static const detailPenerima = _Paths.detailPenerima; static const konfirmasiPenerima = _Paths.konfirmasiPenerima; static const pelaksanaanPenyaluran = _Paths.pelaksanaanPenyaluran; + static const profile = _Paths.profile; } abstract class _Paths { @@ -32,4 +33,5 @@ abstract class _Paths { static const detailPenerima = '/daftar-penerima/detail'; static const konfirmasiPenerima = '/daftar-penerima/konfirmasi'; static const pelaksanaanPenyaluran = '/pelaksanaan-penyaluran'; + static const profile = '/profile'; } diff --git a/lib/app/services/auth_service.dart b/lib/app/services/auth_service.dart new file mode 100644 index 0000000..5fe8c92 --- /dev/null +++ b/lib/app/services/auth_service.dart @@ -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 currentUser = Rx(null); + + // Mendapatkan data user saat ini + Future 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 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 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 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 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 init() async { + // Coba mendapatkan user saat ini + await getCurrentUser(); + return this; + } +} diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index 80b6e0b..37a2932 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -6,6 +6,9 @@ class SupabaseService extends GetxService { late final SupabaseClient client; + // Cache untuk profil pengguna + Map? _cachedUserProfile; + // Ganti dengan URL dan API key Supabase Anda static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL', defaultValue: 'http://labulabs.net:8000'); @@ -42,6 +45,7 @@ class SupabaseService extends GetxService { // Metode untuk logout Future signOut() async { + _cachedUserProfile = null; // Hapus cache saat logout await client.auth.signOut(); } @@ -53,15 +57,37 @@ class SupabaseService extends GetxService { // Metode untuk mendapatkan profil pengguna Future?> getUserProfile() async { - if (currentUser == null) return null; + final user = currentUser; + if (user == null) return null; - final response = await client - .from('user_profile') - .select() - .eq('id', currentUser!.id) - .maybeSingle(); + try { + // Gunakan cache jika tersedia + if (_cachedUserProfile != null && _cachedUserProfile!['id'] == user.id) { + print('Menggunakan data profil dari cache'); + return _cachedUserProfile; + } - return response; + final response = await client + .from('user_profile') + .select('*, desa:desa_id(id, nama, kecamatan, kabupaten, provinsi)') + .eq('id', user.id) + .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; + } catch (e) { + print('Error pada getUserProfile: $e'); + return null; + } } // Metode untuk mendapatkan role pengguna @@ -70,64 +96,491 @@ class SupabaseService extends GetxService { return profile?['role']; } - // Metode untuk mendapatkan data berdasarkan peran - Future?> getRoleSpecificData(String role) async { - if (currentUser == null) return null; + // ==================== PETUGAS DESA METHODS ==================== - switch (role) { - case 'WARGA': - return await getWargaByUserId(); - case 'PETUGASVERIFIKASI': - return await getPetugasVerifikasiData(); - case 'PETUGASDESA': - return await getPetugasDesaData(); - case 'DONATUR': - return await getDonaturData(); - default: - return null; + // Dashboard methods + Future getTotalPenerima() async { + try { + final response = + await client.from('warga').select('id').eq('status', 'AKTIF'); + + return response.length; + } catch (e) { + print('Error getting total penerima: $e'); + return null; } } - // Metode untuk mendapatkan data petugas verifikasi - Future?> getPetugasVerifikasiData() async { - if (currentUser == null) return null; + Future getTotalBantuan() async { + try { + final response = await client.from('stok_bantuan').select('jumlah'); - final response = await client - .from('xx02_PetugasVerifikasi') - .select() - .eq('userId', currentUser!.id) - .maybeSingle(); + double total = 0; + for (var item in response) { + total += (item['jumlah'] ?? 0); + } - return response; + return total.toInt(); + } catch (e) { + print('Error getting total bantuan: $e'); + return null; + } } - // Metode untuk mendapatkan data petugas desa - Future?> getPetugasDesaData() async { - if (currentUser == null) return null; + Future getTotalPenyaluran() async { + try { + final response = await client + .from('penyaluran_bantuan') + .select('id') + .eq('status', 'SELESAI'); - final response = await client - .from('xx01_PetugasDesa') - .select() - .eq('userId', currentUser!.id) - .maybeSingle(); - - return response; + return response.length; + } catch (e) { + print('Error getting total penyaluran: $e'); + return null; + } } - // Metode untuk mendapatkan data donatur - Future?> getDonaturData() async { - if (currentUser == null) return null; + Future>?> getNotifikasiBelumDibaca( + String userId) async { + try { + final response = await client + .from('notifikasi') + .select('*') + .eq('user_id', userId) + .eq('dibaca', false) + .order('created_at', ascending: false); - final response = await client - .from('xx01_Donatur') - .select() - .eq('userId', currentUser!.id) - .maybeSingle(); - - return response; + return response; + } catch (e) { + print('Error getting notifikasi belum dibaca: $e'); + return null; + } } - // Metode untuk membuat data warga + // Jadwal penyaluran methods + Future>?> 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 + .from('penyaluran_bantuan') + .select('*') + .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>?> 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>?> 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>?> 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 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 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 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>?> getStokBantuan() async { + try { + final response = await client.from('stok_bantuan').select('*'); + + return response; + } catch (e) { + print('Error getting stok bantuan: $e'); + return null; + } + } + + Future?> 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>?> getBentukBantuan() async { + try { + final response = await client.from('bentuk_bantuan').select('*'); + + return response; + } catch (e) { + print('Error getting bentuk bantuan: $e'); + return null; + } + } + + Future addStok(Map stok) async { + try { + await client.from('stok_bantuan').insert(stok); + } catch (e) { + print('Error adding stok: $e'); + throw e.toString(); + } + } + + Future updateStok(String stokId, Map stok) async { + try { + await client.from('stok_bantuan').update(stok).eq('id', stokId); + } catch (e) { + print('Error updating stok: $e'); + throw e.toString(); + } + } + + Future 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>?> getPenitipanBantuan() async { + try { + final response = await client.from('penitipan_bantuan').select('*'); + + return response; + } catch (e) { + print('Error getting penitipan bantuan: $e'); + return null; + } + } + + Future 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 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?> 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>?> getPengaduan() async { + try { + final response = await client.from('pengaduan').select('*'); + + return response; + } catch (e) { + print('Error getting pengaduan: $e'); + return null; + } + } + + Future 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 tambahTindakanPengaduan(Map tindakan) async { + try { + await client.from('tindakan_pengaduan').insert(tindakan); + } catch (e) { + print('Error adding tindakan pengaduan: $e'); + throw e.toString(); + } + } + + Future 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>?> 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>?> getPenerimaBantuan() async { + try { + final response = await client.from('warga').select('*'); + + return response; + } catch (e) { + print('Error getting penerima bantuan: $e'); + return null; + } + } + + Future tambahPenerima(Map penerima) async { + try { + await client.from('warga').insert(penerima); + } catch (e) { + print('Error adding penerima: $e'); + throw e.toString(); + } + } + + Future updatePenerima( + String penerimaId, Map penerima) async { + try { + await client.from('warga').update(penerima).eq('id', penerimaId); + } catch (e) { + print('Error updating penerima: $e'); + throw e.toString(); + } + } + + Future 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>?> 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 generateLaporan(Map 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 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 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?> getWargaByUserId() async { + try { + final user = currentUser; + if (user == null) return null; + + final response = await client + .from('warga') + .select('*') + .eq('user_id', user.id) + .maybeSingle(); + + return response; + } catch (e) { + print('Error getting warga data: $e'); + return null; + } + } + + // Metode untuk membuat profil warga Future createWargaProfile({ required String nik, required String namaLengkap, @@ -138,131 +591,75 @@ class SupabaseService extends GetxService { DateTime? tanggalLahir, String? agama, }) async { - if (currentUser == null) return; + try { + final user = currentUser; + if (user == null) throw 'User tidak ditemukan'; - await client.from('xx02_Warga').insert({ - 'NIK': nik, - 'namaLengkap': namaLengkap, - 'jenisKelamin': jenisKelamin, - 'noHp': noHp, - 'alamat': alamat, - 'tempatLahir': tempatLahir, - 'tanggalLahir': tanggalLahir?.toIso8601String(), - 'agama': agama, - 'userId': currentUser!.id, - 'email': currentUser!.email, - }); - } + await client.from('warga').insert({ + 'user_id': user.id, + 'nik': nik, + 'nama_lengkap': namaLengkap, + 'jenis_kelamin': jenisKelamin, + 'no_hp': noHp, + 'alamat': alamat, + 'tempat_lahir': tempatLahir, + 'tanggal_lahir': tanggalLahir?.toIso8601String(), + 'agama': agama, + 'status': 'MENUNGGU_VERIFIKASI', + 'created_at': DateTime.now().toIso8601String(), + 'updated_at': DateTime.now().toIso8601String(), + }); - // Metode untuk mendapatkan data warga berdasarkan userId - Future?> getWargaByUserId() async { - if (currentUser == null) return null; - - final response = await client - .from('xx02_Warga') - .select() - .eq('userId', currentUser!.id) - .maybeSingle(); - - return response; + // 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 notifikasi pengguna Future>> getUserNotifications( {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 filteredQuery = query.eq('userId', currentUser!.id); - - // Tambahkan filter untuk notifikasi yang belum dibaca jika diperlukan - final finalQuery = - unreadOnly ? filteredQuery.eq('isRead', false) : filteredQuery; - - // Tambahkan pengurutan - final response = await finalQuery.order('CREATED_AT', ascending: false); - - return List>.from(response); + final response = await query; + return response; + } catch (e) { + print('Error getting user notifications: $e'); + return []; + } } // Metode untuk menandai notifikasi sebagai telah dibaca Future markNotificationAsRead(int notificationId) async { - await client - .from('Notification') - .update({'isRead': true}).eq('notificationId', notificationId); - } - - // Metode untuk mendapatkan data verifikasi warga - Future>> getVerifikasiDataWarga() async { - if (currentUser == null) return []; - - final response = await client - .from('xx02_VerifikasiDataWarga') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); - } - - // Metode untuk mendapatkan data pengajuan bantuan - Future>> getPengajuanBantuan() async { - if (currentUser == null) return []; - - final response = await client - .from('xx02_PengajuanKelayakanBantuan') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); - } - - // Metode untuk mendapatkan data skema bantuan - Future>> getSkemaBantuan() async { - if (currentUser == null) return []; - - final response = await client - .from('xx02_SkemaBantuan') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); - } - - // Metode untuk mendapatkan data penyaluran bantuan - Future>> getPenyaluranBantuan() async { - if (currentUser == null) return []; - - final response = await client - .from('xx01_PenyaluranBantuan') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); - } - - // Metode untuk mendapatkan data penitipan bantuan - Future>> getPenitipanBantuan() async { - if (currentUser == null) return []; - - final response = await client - .from('xx01_PenitipanBantuan') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); - } - - // Metode untuk mendapatkan data pengaduan - Future>> getPengaduan() async { - if (currentUser == null) return []; - - final response = await client - .from('xx01_Pengaduan') - .select() - .order('CREATED_AT', ascending: false); - - return List>.from(response); + try { + await client.from('notifikasi').update({ + 'dibaca': true, + 'updated_at': DateTime.now().toIso8601String(), + }).eq('id', notificationId); + } catch (e) { + print('Error marking notification as read: $e'); + throw e.toString(); + } } } diff --git a/lib/app/utils/date_formatter.dart b/lib/app/utils/date_formatter.dart new file mode 100644 index 0000000..6752e02 --- /dev/null +++ b/lib/app/utils/date_formatter.dart @@ -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 + } + } +} diff --git a/lib/app/widgets/navigation_button.dart b/lib/app/widgets/navigation_button.dart index 24c0169..f60254c 100644 --- a/lib/app/widgets/navigation_button.dart +++ b/lib/app/widgets/navigation_button.dart @@ -8,14 +8,13 @@ class NavigationButton extends StatelessWidget { final VoidCallback onPressed; const NavigationButton({ - Key? key, + super.key, required this.label, this.icon, this.iconWidget, required this.onPressed, - }) : assert(icon != null || iconWidget != null, - 'Either icon or iconWidget must be provided'), - super(key: key); + }) : assert(icon != null || iconWidget != null, + 'Either icon or iconWidget must be provided'); @override Widget build(BuildContext context) { diff --git a/lib/app/widgets/statistic_card.dart b/lib/app/widgets/statistic_card.dart index 055de2b..931a9fa 100644 --- a/lib/app/widgets/statistic_card.dart +++ b/lib/app/widgets/statistic_card.dart @@ -8,12 +8,12 @@ class StatisticCard extends StatelessWidget { final double height; const StatisticCard({ - Key? key, + super.key, required this.title, required this.count, required this.subtitle, required this.height, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/main.dart b/lib/main.dart index 50ea042..32aff6e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.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/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 { WidgetsFlutterBinding.ensureInitialized(); + // Inisialisasi data locale untuk format tanggal + await initializeDateFormatting('id_ID', null); + // Inisialisasi Supabase await initServices(); @@ -16,6 +23,10 @@ void main() async { // Inisialisasi service Future initServices() async { await Get.putAsync(() => SupabaseService().init()); + await Get.putAsync(() => AuthService().init()); + + // Inisialisasi AuthController secara global + Get.put(AuthController(), permanent: true); } class MyApp extends StatelessWidget { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3792af4..5a27a5d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d07423..98d181b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux gtk url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 92b6497..ee5d20f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import app_links +import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 70a812c..583c64a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -134,6 +150,54 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -232,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" jwt_decode: dependency: transitive description: @@ -701,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.dev" + source: hosted + version: "5.10.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 69f2068..64e2c36 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,12 @@ dependencies: # Untuk format tanggal dalam bahasa Indonesia intl: ^0.19.0 + # HTTP client + dio: ^5.4.1 + + # Secure storage + flutter_secure_storage: ^9.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 785a046..563050d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8f8ee4f..a3836b2 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + flutter_secure_storage_windows url_launcher_windows )