ambil data stok bantuan

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

View File

@ -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<void> 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<void> login() async {
if (!loginFormKey.currentState!.validate()) return;
print('DEBUG: Memulai proses login');
if (loginFormKey.currentState == null) {
print('Error: loginFormKey.currentState adalah null');
print('DEBUG: Form key: $loginFormKey');
Get.snackbar(
'Error',
'Terjadi kesalahan pada form login. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
print('DEBUG: Form state ditemukan, melakukan validasi');
if (!loginFormKey.currentState!.validate()) {
print('DEBUG: Validasi form gagal');
return;
}
// Simpan nilai dari controller sebelum melakukan operasi asinkron
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

View File

@ -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<AuthController>()) {
Get.put(AuthController(), permanent: true);
}
Get.lazyPut<PenerimaController>(
() => PenerimaController(),
fenix: true,

View File

@ -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<AuthController>()) {
Get.put(AuthController(), permanent: true);
}
// Main controller
Get.lazyPut<PetugasDesaController>(
() => PetugasDesaController(),
fenix: true,
);
Get.lazyPut<PenerimaController>(
() => PenerimaController(),
// Dashboard controller
Get.lazyPut<PetugasDesaDashboardController>(
() => PetugasDesaDashboardController(),
);
// Jadwal penyaluran controller
Get.lazyPut<JadwalPenyaluranController>(
() => JadwalPenyaluranController(),
);
// Stok bantuan controller
Get.lazyPut<StokBantuanController>(
() => StokBantuanController(),
);
// Penitipan bantuan controller
Get.lazyPut<PenitipanBantuanController>(
() => PenitipanBantuanController(),
);
// Pengaduan controller
Get.lazyPut<PengaduanController>(
() => PengaduanController(),
);
// Penerima bantuan controller
Get.lazyPut<PenerimaBantuanController>(
() => PenerimaBantuanController(),
);
// Laporan controller
Get.lazyPut<LaporanController>(
() => LaporanController(),
);
}
}

View File

@ -43,7 +43,9 @@ class GreetingHeader extends StatelessWidget {
),
const SizedBox(height: 5),
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],

View File

@ -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<Map<String, dynamic>> jadwalList;
final List<dynamic> 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<Map<String, dynamic>> _getCurrentJadwalList() {
List<dynamic> _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<String, dynamic> jadwal) {
Widget _buildJadwalItem(TextTheme textTheme, dynamic jadwal) {
// Konversi jadwal ke Map jika itu adalah PenyaluranBantuanModel
final Map<String, dynamic> jadwalData =
jadwal is Map<String, dynamic> ? jadwal : jadwal.toJson();
Color statusColor;
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,
),
],

View File

@ -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<String, dynamic> permintaan) {
Widget _buildPermintaanPreview(TextTheme textTheme, dynamic permintaan) {
// Konversi permintaan ke Map jika itu adalah PenyaluranBantuanModel
final Map<String, dynamic> permintaanData =
permintaan is Map<String, dynamic> ? permintaan : permintaan.toJson();
return Container(
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,
),

View File

@ -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<String, dynamic> 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<String, dynamic> permintaan) {
void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) {
String? selectedJadwalId;
// Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>(
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<String, dynamic> 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(),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package: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<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
final RxBool isLoading = false.obs;
final Rx<Map<String, dynamic>?> roleData = Rx<Map<String, dynamic>?>(null);
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
// Indeks tab yang aktif di bottom navigation bar
final RxInt activeTabIndex = 0.obs;
// Data untuk dashboard
final RxInt totalPenerima = 0.obs;
final RxInt totalBantuan = 0.obs;
final RxInt totalPenyaluran = 0.obs;
final RxDouble progressPenyaluran = 0.0.obs;
// Data untuk jadwal
final RxList<Map<String, dynamic>> jadwalHariIni =
<Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> jadwalMendatang =
<Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> jadwalSelesai =
<Map<String, dynamic>>[].obs;
// Data untuk permintaan penjadwalan
final RxList<Map<String, dynamic>> permintaanPenjadwalan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahPermintaanPenjadwalan = 0.obs;
// Data untuk notifikasi
final RxList<Map<String, dynamic>> notifikasiBelumDibaca =
<Map<String, dynamic>>[].obs;
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Data untuk inventaris
final RxList<Map<String, dynamic>> daftarInventaris =
<Map<String, dynamic>>[].obs;
final RxDouble totalStok = 0.0.obs;
final RxDouble stokMasuk = 0.0.obs;
final RxDouble stokKeluar = 0.0.obs;
// Data untuk penitipan
final RxList<Map<String, dynamic>> daftarPenitipan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahMenunggu = 0.obs;
final RxInt jumlahTerverifikasi = 0.obs;
final RxInt jumlahDitolak = 0.obs;
// Data untuk pengaduan
final RxList<Map<String, dynamic>> daftarPengaduan =
<Map<String, dynamic>>[].obs;
final RxInt jumlahDiproses = 0.obs;
final RxInt jumlahTindakan = 0.obs;
final RxInt jumlahSelesai = 0.obs;
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
// Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
// Model desa dari cache
final Rx<DesaModel?> desaModel = Rx<DesaModel?>(null);
// Counter untuk notifikasi
final RxInt jumlahNotifikasiBelumDibaca = 0.obs;
// Counter untuk permintaan menunggu
final RxInt jumlahMenunggu = 0.obs;
// Counter untuk pengaduan yang diproses
final RxInt jumlahDiproses = 0.obs;
// Data jadwal hari ini
final RxList<dynamic> jadwalHariIni = <dynamic>[].obs;
UserModel? get user => _authController.user;
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<void> loadRoleData() async {
isLoading.value = true;
// Metode untuk memuat data profil pengguna dari cache
Future<void> loadUserProfile() async {
try {
// Jika user sudah ada di AuthController, tidak perlu mengambil data lagi
if (user != null) {
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<String, dynamic>) {
try {
final desaData = profileData['desa'] as Map<String, dynamic>;
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<void> 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<void> loadJadwalData() async {
try {
// Simulasi data untuk jadwal
await Future.delayed(const Duration(milliseconds: 600));
jadwalHariIni.value = [
{
'id': '1',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '15 April 2023',
'waktu': '09:00 - 12:00',
'status': 'aktif',
'jumlah_penerima': 45,
},
{
'id': '2',
'lokasi': 'Pos RW 03',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '15 April 2023',
'waktu': '13:00 - 15:00',
'status': 'aktif',
'jumlah_penerima': 30,
},
];
jadwalMendatang.value = [
{
'id': '3',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '22 April 2023',
'waktu': '09:00 - 12:00',
'status': 'terjadwal',
'jumlah_penerima': 50,
},
{
'id': '4',
'lokasi': 'Pos RW 05',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '23 April 2023',
'waktu': '13:00 - 15:00',
'status': 'terjadwal',
'jumlah_penerima': 35,
},
];
jadwalSelesai.value = [
{
'id': '5',
'lokasi': 'Balai Desa Sukamaju',
'jenis_bantuan': 'Beras',
'tanggal': '8 April 2023',
'waktu': '09:00 - 12:00',
'status': 'selesai',
'jumlah_penerima': 48,
},
{
'id': '6',
'lokasi': 'Pos RW 02',
'jenis_bantuan': 'Paket Sembako',
'tanggal': '9 April 2023',
'waktu': '13:00 - 15:00',
'status': 'selesai',
'jumlah_penerima': 32,
},
];
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getJadwalData();
// jadwalHariIni.value = result['hari_ini'] ?? [];
// jadwalMendatang.value = result['mendatang'] ?? [];
// jadwalSelesai.value = result['selesai'] ?? [];
} catch (e) {
print('Error loading jadwal data: $e');
}
}
Future<void> loadPermintaanPenjadwalanData() async {
try {
// Simulasi data untuk permintaan penjadwalan
await Future.delayed(const Duration(milliseconds: 600));
permintaanPenjadwalan.value = [
{
'id': '1',
'nama': 'Ahmad Sulaiman',
'nik': '3201234567890001',
'jenis_bantuan': 'Beras',
'tanggal_permintaan': '14 April 2023',
'alamat': 'Dusun Sukamaju RT 02/03',
'status': 'menunggu',
},
{
'id': '2',
'nama': 'Siti Aminah',
'nik': '3201234567890002',
'jenis_bantuan': 'Sembako',
'tanggal_permintaan': '13 April 2023',
'alamat': 'Dusun Sukamaju RT 01/03',
'status': 'menunggu',
},
];
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diambil dari Supabase
// final result = await _supabaseService.getPermintaanPenjadwalanData();
// permintaanPenjadwalan.value = result ?? [];
// jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
} catch (e) {
print('Error loading permintaan penjadwalan data: $e');
print('Error loading user profile: $e');
}
}
// Metode untuk memuat data notifikasi
Future<void> loadNotifikasiData() async {
try {
// Simulasi data untuk notifikasi
await Future.delayed(const Duration(milliseconds: 500));
// Hitung jumlah notifikasi yang belum dibaca
final List<Map<String, dynamic>> 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<void> loadInventarisData() async {
// Metode untuk memuat data jadwal
Future<void> 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<void> 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<void> 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<String, dynamic> data) {
// Implementasi untuk menambah inventaris
// Di implementasi nyata, akan memanggil Supabase untuk menambah data inventaris
// await _supabaseService.addInventory(data);
// Perbarui data lokal
loadInventarisData();
}
void hapusInventaris(String id) {
// Implementasi untuk menghapus inventaris
// Di implementasi nyata, akan memanggil Supabase untuk menghapus data inventaris
// await _supabaseService.deleteInventory(id);
// Perbarui data lokal
loadInventarisData();
}
void terimaPermohonanPenitipan(String id) {
// Implementasi untuk menerima permohonan penitipan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status penitipan
// await _supabaseService.acceptDeposit(id);
// Perbarui data lokal
loadPenitipanData();
loadInventarisData(); // Perbarui inventaris karena ada penambahan stok
}
void tolakPermohonanPenitipan(String id) {
// Implementasi untuk menolak permohonan penitipan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status penitipan
// await _supabaseService.rejectDeposit(id);
// Perbarui data lokal
loadPenitipanData();
}
void prosesPengaduan(String id, String tindakan) {
// Implementasi untuk memproses pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.processPengaduan(id, tindakan);
// Perbarui data lokal
loadPengaduanData();
}
void selesaikanPengaduan(String id, String hasil) {
// Implementasi untuk menyelesaikan pengaduan
// Di implementasi nyata, akan memanggil Supabase untuk memperbarui status pengaduan
// await _supabaseService.completePengaduan(id, hasil);
// Perbarui data lokal
loadPengaduanData();
}
void logout() {
_authController.logout();
// Metode untuk memperbarui counter pengaduan
Future<void> 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<void> 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<void> tolakPermintaanPenjadwalan(String id, String alasan) async {
try {
if (id.isEmpty) {
Get.snackbar(
'Error',
'ID permintaan tidak valid',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
isLoading.value = true;
// Simulasi proses penolakan
await Future.delayed(const Duration(milliseconds: 800));
// Hapus permintaan dari daftar
permintaanPenjadwalan.removeWhere((item) => item['id'] == id);
jumlahPermintaanPenjadwalan.value = permintaanPenjadwalan.length;
// Di implementasi nyata, data akan diupdate ke Supabase
// await _supabaseService.tolakPermintaanPenjadwalan(id, alasan);
// await loadPermintaanPenjadwalanData();
} catch (e) {
print('Error tolak permintaan penjadwalan: $e');
Get.snackbar(
'Error',
'Terjadi kesalahan saat menolak permintaan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
// Metode untuk logout
Future<void> logout() async {
await _authController.logout();
}
}

View File

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

View File

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

View File

@ -22,9 +22,9 @@ class DashboardView extends GetView<PetugasDesaController> {
children: [
// 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),

View File

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

View File

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

View File

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

View File

@ -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<PetugasDesaController> {
class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key});
@override

View File

@ -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<PetugasDesaController> {
class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
const PermintaanPenjadwalanView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Pastikan controller sudah diinisialisasi
if (!Get.isRegistered<PetugasDesaController>()) {
Get.put(PetugasDesaController());
if (!Get.isRegistered<JadwalPenyaluranController>()) {
Get.put(JadwalPenyaluranController());
}
return Scaffold(
@ -236,7 +237,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
);
}
Widget _buildPermintaanItem(BuildContext context, Map<String, dynamic> item) {
Widget _buildPermintaanItem(BuildContext context, PenyaluranBantuanModel item) {
Color statusColor = Colors.orange;
IconData statusIcon = Icons.pending_actions;
@ -265,7 +266,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
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<PetugasDesaController> {
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<PetugasDesaController> {
context,
icon: Icons.category,
label: 'Jenis Bantuan',
value: item['jenis_bantuan'] ?? '',
value: item.judul ?? '',
),
),
],
@ -329,15 +330,15 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
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<PetugasDesaController> {
}
// Dialog untuk konfirmasi permintaan
void _showKonfirmasiDialog(Map<String, dynamic> permintaan) {
void _showKonfirmasiDialog(PenyaluranBantuanModel permintaan) {
String? selectedJadwalId;
// Data jadwal yang tersedia dari controller
final jadwalOptions = controller.jadwalMendatang.map((jadwal) {
return DropdownMenuItem<String>(
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<PetugasDesaController> {
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<PetugasDesaController> {
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<PetugasDesaController> {
}
// Dialog untuk menolak permintaan
void _showTolakDialog(Map<String, dynamic> permintaan) {
void _showTolakDialog(PenyaluranBantuanModel permintaan) {
final TextEditingController alasanController = TextEditingController();
Get.dialog(
@ -519,7 +519,7 @@ class PermintaanPenjadwalanView extends GetView<PetugasDesaController> {
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<PetugasDesaController> {
onPressed: () {
if (alasanController.text.trim().isNotEmpty) {
// Panggil metode tolak di controller
controller.tolakPermintaanPenjadwalan(
permintaan['id'] ?? '',
controller.rejectJadwal(
permintaan.id ?? '',
alasanController.text.trim(),
);

View File

@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
import 'package:penyaluran_app/app/modules/petugas_desa/views/dashboard_view.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/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<PetugasDesaController> {
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<PetugasDesaController> {
],
);
// 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<PetugasDesaController> {
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<PetugasDesaController> {
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<PetugasDesaController> {
}),
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<PetugasDesaController> {
onTap: () {
// Navigasi ke halaman profil
Navigator.pop(context);
Get.toNamed('/profile');
},
),
ListTile(
@ -623,7 +624,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
const BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined),
activeIcon: Icon(Icons.inventory_2),
label: 'Inventaris',
label: 'Stok Bantuan',
),
],
);

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
class SplashView extends StatefulWidget {
const SplashView({Key? key}) : super(key: key);
const SplashView({super.key});
@override
State<SplashView> createState() => _SplashViewState();