Refactor stok bantuan model dan kontroller untuk mendukung kategori bantuan

- Ubah model StokBantuanModel dari 'jenis bantuan' menjadi 'kategori bantuan'
- Perbarui metode loadJenisBantuanData() menjadi loadKategoriBantuanData()
- Tambahkan metode baru untuk menghitung stok hampir habis dan segera kadaluarsa
- Update tampilan dan form untuk menggunakan kategori bantuan
- Perbaiki logika navigasi dan binding pada berbagai modul terkait
This commit is contained in:
Khafidh Fuadi
2025-03-11 22:14:07 +07:00
parent cdbd659d63
commit f7397cb9cf
12 changed files with 596 additions and 408 deletions

View File

@ -3,14 +3,11 @@ import 'dart:convert';
class StokBantuanModel { class StokBantuanModel {
final String? id; final String? id;
final String? nama; final String? nama;
final String? bentukBantuanId; final String? kategoriBantuanId;
final String? sumberBantuanId; final Map<String, dynamic>? kategoriBantuan;
final String? jenisBantuanId;
final Map<String, dynamic>? jenisBantuan;
final double? jumlah; final double? jumlah;
final String? satuan; final String? satuan;
final String? deskripsi; final String? deskripsi;
final String? status;
final DateTime? tanggalMasuk; final DateTime? tanggalMasuk;
final DateTime? tanggalKadaluarsa; final DateTime? tanggalKadaluarsa;
final DateTime? createdAt; final DateTime? createdAt;
@ -19,14 +16,11 @@ class StokBantuanModel {
StokBantuanModel({ StokBantuanModel({
this.id, this.id,
this.nama, this.nama,
this.bentukBantuanId, this.kategoriBantuanId,
this.sumberBantuanId, this.kategoriBantuan,
this.jenisBantuanId,
this.jenisBantuan,
this.jumlah, this.jumlah,
this.satuan, this.satuan,
this.deskripsi, this.deskripsi,
this.status,
this.tanggalMasuk, this.tanggalMasuk,
this.tanggalKadaluarsa, this.tanggalKadaluarsa,
this.createdAt, this.createdAt,
@ -42,14 +36,11 @@ class StokBantuanModel {
StokBantuanModel( StokBantuanModel(
id: json["id"], id: json["id"],
nama: json["nama"], nama: json["nama"],
bentukBantuanId: json["bentuk_bantuan_id"], kategoriBantuanId: json["kategori_bantuan_id"],
sumberBantuanId: json["sumber_bantuan_id"], kategoriBantuan: json["kategori_bantuan"],
jenisBantuanId: json["jenis_bantuan_id"],
jenisBantuan: json["jenis_bantuan"],
jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0,
satuan: json["satuan"], satuan: json["satuan"],
deskripsi: json["deskripsi"], deskripsi: json["deskripsi"],
status: json["status"],
tanggalMasuk: json["tanggal_masuk"] != null tanggalMasuk: json["tanggal_masuk"] != null
? DateTime.parse(json["tanggal_masuk"]) ? DateTime.parse(json["tanggal_masuk"])
: null, : null,
@ -64,19 +55,24 @@ class StokBantuanModel {
: null, : null,
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() {
"id": id, final Map<String, dynamic> data = {
"nama": nama, "nama": nama,
"bentuk_bantuan_id": bentukBantuanId, "kategori_bantuan_id": kategoriBantuanId,
"sumber_bantuan_id": sumberBantuanId, "jumlah": jumlah,
"jenis_bantuan_id": jenisBantuanId, "satuan": satuan,
"jumlah": jumlah, "deskripsi": deskripsi,
"satuan": satuan, "tanggal_masuk": tanggalMasuk?.toIso8601String(),
"deskripsi": deskripsi, "tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(),
"status": status, "created_at": createdAt?.toIso8601String(),
"tanggal_masuk": tanggalMasuk?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
"tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(), };
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), // Tambahkan id hanya jika tidak null
}; if (id != null) {
data["id"] = id;
}
return data;
}
} }

View File

@ -116,9 +116,14 @@ class AuthController extends GetxController {
final targetRoute = _getTargetRouteForRole(role); final targetRoute = _getTargetRouteForRole(role);
print('Target rute: $targetRoute'); print('Target rute: $targetRoute');
if (currentRoute != targetRoute) { // Jika berada di splash atau login, navigasi ke dashboard
if (currentRoute == Routes.splash || currentRoute == Routes.login) {
print('Navigasi ke rute target berdasarkan role'); print('Navigasi ke rute target berdasarkan role');
navigateBasedOnRole(role); navigateBasedOnRole(role);
} else if (currentRoute != targetRoute) {
// Jika berada di rute lain yang tidak sesuai dengan role, navigasi ke dashboard
print('Berada di rute yang tidak sesuai, navigasi ke rute target');
navigateBasedOnRole(role);
} else { } else {
print('Sudah berada di rute yang sesuai, tidak perlu navigasi'); print('Sudah berada di rute yang sesuai, tidak perlu navigasi');
} }
@ -334,7 +339,7 @@ class AuthController extends GetxController {
} }
} }
// Mendapatkan rute target berdasarkan peran // Mendapatkan rute target berdasarkan role
String _getTargetRouteForRole(String role) { String _getTargetRouteForRole(String role) {
switch (role) { switch (role) {
case 'WARGA': case 'WARGA':

View File

@ -17,45 +17,54 @@ class PetugasDesaBinding extends Bindings {
Get.put(AuthController(), permanent: true); Get.put(AuthController(), permanent: true);
} }
// Main controller // Main controller - gunakan put dengan permanent untuk controller utama
Get.lazyPut<PetugasDesaController>( if (!Get.isRegistered<PetugasDesaController>()) {
() => PetugasDesaController(), Get.put(PetugasDesaController(), permanent: true);
fenix: true, } else {
); // Jika sudah terdaftar, gunakan find untuk mendapatkan instance yang ada
Get.find<PetugasDesaController>();
}
// Dashboard controller // Dashboard controller
Get.lazyPut<PetugasDesaDashboardController>( Get.lazyPut<PetugasDesaDashboardController>(
() => PetugasDesaDashboardController(), () => PetugasDesaDashboardController(),
fenix: true,
); );
// Jadwal penyaluran controller // Jadwal penyaluran controller
Get.lazyPut<JadwalPenyaluranController>( Get.lazyPut<JadwalPenyaluranController>(
() => JadwalPenyaluranController(), () => JadwalPenyaluranController(),
fenix: true,
); );
// Stok bantuan controller // Stok bantuan controller
Get.lazyPut<StokBantuanController>( Get.lazyPut<StokBantuanController>(
() => StokBantuanController(), () => StokBantuanController(),
fenix: true,
); );
// Penitipan bantuan controller // Penitipan bantuan controller
Get.lazyPut<PenitipanBantuanController>( Get.lazyPut<PenitipanBantuanController>(
() => PenitipanBantuanController(), () => PenitipanBantuanController(),
fenix: true,
); );
// Pengaduan controller // Pengaduan controller
Get.lazyPut<PengaduanController>( Get.lazyPut<PengaduanController>(
() => PengaduanController(), () => PengaduanController(),
fenix: true,
); );
// Penerima bantuan controller // Penerima bantuan controller
Get.lazyPut<PenerimaBantuanController>( Get.lazyPut<PenerimaBantuanController>(
() => PenerimaBantuanController(), () => PenerimaBantuanController(),
fenix: true,
); );
// Laporan controller // Laporan controller
Get.lazyPut<LaporanController>( Get.lazyPut<LaporanController>(
() => LaporanController(), () => LaporanController(),
fenix: true,
); );
} }
} }

View File

@ -148,7 +148,7 @@ class JadwalSectionWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Jenis Bantuan: ${jadwalData['jenis_bantuan'] ?? ''}', 'Kategori Bantuan: ${jadwalData['kategori_bantuan'] ?? ''}',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),

View File

@ -181,7 +181,7 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Jenis: ${permintaanData['jenis_bantuan'] ?? ''}', 'Kategori: ${permintaanData['kategori_bantuan'] ?? ''}',
style: textTheme.bodySmall, style: textTheme.bodySmall,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@ -17,8 +17,8 @@ class StokBantuanController extends GetxController {
final RxDouble stokMasuk = 0.0.obs; final RxDouble stokMasuk = 0.0.obs;
final RxDouble stokKeluar = 0.0.obs; final RxDouble stokKeluar = 0.0.obs;
// Data untuk jenis bantuan // Data untuk kategori bantuan
final RxList<Map<String, dynamic>> daftarJenisBantuan = final RxList<Map<String, dynamic>> daftarKategoriBantuan =
<Map<String, dynamic>>[].obs; <Map<String, dynamic>>[].obs;
// Controller untuk pencarian // Controller untuk pencarian
@ -31,7 +31,7 @@ class StokBantuanController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
loadStokBantuanData(); loadStokBantuanData();
loadJenisBantuanData(); loadKategoriBantuanData();
// Listener untuk pencarian // Listener untuk pencarian
searchController.addListener(() { searchController.addListener(() {
@ -74,14 +74,14 @@ class StokBantuanController extends GetxController {
} }
} }
Future<void> loadJenisBantuanData() async { Future<void> loadKategoriBantuanData() async {
try { try {
final jenisBantuanData = await _supabaseService.getJenisBantuan(); final kategoriBantuanData = await _supabaseService.getKategoriBantuan();
if (jenisBantuanData != null) { if (kategoriBantuanData != null) {
daftarJenisBantuan.value = jenisBantuanData; daftarKategoriBantuan.value = kategoriBantuanData;
} }
} catch (e) { } catch (e) {
print('Error loading jenis bantuan data: $e'); print('Error loading kategori bantuan data: $e');
} }
} }
@ -167,7 +167,7 @@ class StokBantuanController extends GetxController {
isLoading.value = true; isLoading.value = true;
try { try {
await loadStokBantuanData(); await loadStokBantuanData();
await loadJenisBantuanData(); await loadKategoriBantuanData();
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -194,4 +194,18 @@ class StokBantuanController extends GetxController {
.toList(); .toList();
} }
} }
// Metode untuk mendapatkan jumlah stok yang hampir habis (stok <= 10)
int getStokHampirHabis() {
return daftarStokBantuan.where((stok) => (stok.jumlah ?? 0) <= 10).length;
}
// Metode untuk mendapatkan jumlah stok yang segera kadaluarsa (dalam 30 hari)
int getStokSegeraKadaluarsa() {
return daftarStokBantuan
.where((stok) =>
stok.tanggalKadaluarsa != null &&
stok.tanggalKadaluarsa!.difference(DateTime.now()).inDays <= 30)
.length;
}
} }

View File

@ -54,8 +54,8 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInfoItem(context, _buildInfoItem(context,
icon: Icons.category, icon: Icons.category,
label: 'Jenis Bantuan', label: 'Kategori Bantuan',
value: jadwal['jenis_bantuan'] ?? '-'), value: jadwal['kategori_bantuan'] ?? '-'),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildInfoItem(context, _buildInfoItem(context,
icon: Icons.calendar_today, icon: Icons.calendar_today,

View File

@ -169,7 +169,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
{ {
'id': '1', 'id': '1',
'donatur': 'PT Sejahtera Abadi', 'donatur': 'PT Sejahtera Abadi',
'jenis_bantuan': 'Sembako', 'kategori_bantuan': 'Sembako',
'jumlah': '500 kg', 'jumlah': '500 kg',
'tanggal_pengajuan': '15 April 2023', 'tanggal_pengajuan': '15 April 2023',
'status': 'Menunggu', 'status': 'Menunggu',
@ -177,7 +177,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
{ {
'id': '2', 'id': '2',
'donatur': 'Yayasan Peduli Sesama', 'donatur': 'Yayasan Peduli Sesama',
'jenis_bantuan': 'Pakaian', 'kategori_bantuan': 'Pakaian',
'jumlah': '200 pcs', 'jumlah': '200 pcs',
'tanggal_pengajuan': '14 April 2023', 'tanggal_pengajuan': '14 April 2023',
'status': 'Terverifikasi', 'status': 'Terverifikasi',
@ -185,7 +185,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
{ {
'id': '3', 'id': '3',
'donatur': 'Bank BRI', 'donatur': 'Bank BRI',
'jenis_bantuan': 'Beras', 'kategori_bantuan': 'Beras',
'jumlah': '300 kg', 'jumlah': '300 kg',
'tanggal_pengajuan': '13 April 2023', 'tanggal_pengajuan': '13 April 2023',
'status': 'Terverifikasi', 'status': 'Terverifikasi',
@ -193,7 +193,7 @@ class PenitipanView extends GetView<PetugasDesaController> {
{ {
'id': '4', 'id': '4',
'donatur': 'Komunitas Peduli', 'donatur': 'Komunitas Peduli',
'jenis_bantuan': 'Alat Tulis', 'kategori_bantuan': 'Alat Tulis',
'jumlah': '100 set', 'jumlah': '100 set',
'tanggal_pengajuan': '12 April 2023', 'tanggal_pengajuan': '12 April 2023',
'status': 'Ditolak', 'status': 'Ditolak',
@ -304,8 +304,8 @@ class PenitipanView extends GetView<PetugasDesaController> {
child: _buildItemDetail( child: _buildItemDetail(
context, context,
icon: Icons.category, icon: Icons.category,
label: 'Jenis Bantuan', label: 'Kategori Bantuan',
value: item['jenis_bantuan'] ?? '', value: item['kategori_bantuan'] ?? '',
), ),
), ),
Expanded( Expanded(

View File

@ -23,7 +23,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
_showAddStokDialog(context); _showAddStokDialog(context);
}, },
backgroundColor: AppTheme.primaryColor, backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.add), child: const Icon(Icons.add, color: Colors.white),
), ),
); );
} }
@ -79,7 +79,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
child: _buildSummaryItem( child: _buildSummaryItem(
context, context,
icon: Icons.inventory_2_outlined, icon: Icons.inventory_2_outlined,
title: 'Total Stok', title: 'Stok Tersedia',
value: DateFormatter.formatNumber(controller.totalStok.value), value: DateFormatter.formatNumber(controller.totalStok.value),
), ),
), ),
@ -87,7 +87,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
child: _buildSummaryItem( child: _buildSummaryItem(
context, context,
icon: Icons.input, icon: Icons.input,
title: 'Masuk', title: 'Total Masuk',
value: DateFormatter.formatNumber(controller.stokMasuk.value), value: DateFormatter.formatNumber(controller.stokMasuk.value),
), ),
), ),
@ -95,13 +95,48 @@ class StokBantuanView extends GetView<StokBantuanController> {
child: _buildSummaryItem( child: _buildSummaryItem(
context, context,
icon: Icons.output, icon: Icons.output,
title: 'Keluar', title: 'Total Keluar',
value: value:
DateFormatter.formatNumber(controller.stokKeluar.value), DateFormatter.formatNumber(controller.stokKeluar.value),
), ),
), ),
], ],
), ),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.warning_amber_rounded,
title: 'Hampir Habis',
value: '${controller.getStokHampirHabis()}',
valueColor: controller.getStokHampirHabis() > 0
? Colors.amber
: Colors.white,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.access_time,
title: 'Segera Kadaluarsa',
value: '${controller.getStokSegeraKadaluarsa()}',
valueColor: controller.getStokSegeraKadaluarsa() > 0
? Colors.amber
: Colors.white,
),
),
Expanded(
child: _buildSummaryItem(
context,
icon: Icons.category_outlined,
title: 'Kategori Bantuan',
value: '${controller.daftarKategoriBantuan.length}',
),
),
],
),
], ],
), ),
); );
@ -112,6 +147,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
required IconData icon, required IconData icon,
required String title, required String title,
required String value, required String value,
Color? valueColor,
}) { }) {
return Column( return Column(
children: [ children: [
@ -132,7 +168,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
value, value,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: valueColor ?? Colors.white,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -276,9 +312,10 @@ class StokBantuanView extends GetView<StokBantuanController> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
item.jenisBantuan != null item.kategoriBantuan != null
? (item.jenisBantuan!['nama'] ?? 'Tidak Ada Jenis') ? (item.kategoriBantuan!['nama'] ??
: 'Tidak Ada Jenis', 'Tidak Ada Kategori')
: 'Tidak Ada Kategori',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -314,7 +351,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
context, context,
icon: Icons.calendar_today, icon: Icons.calendar_today,
label: 'Tanggal Masuk', label: 'Tanggal Masuk',
value: DateFormatter.formatDate(item.tanggalMasuk), value: DateFormatter.formatDateTime(item.tanggalMasuk),
), ),
), ),
], ],
@ -335,7 +372,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
context, context,
icon: Icons.access_time, icon: Icons.access_time,
label: 'Terakhir Diperbarui', label: 'Terakhir Diperbarui',
value: DateFormatter.formatDate(item.updatedAt), value: DateFormatter.formatDateTime(item.updatedAt),
), ),
), ),
], ],
@ -419,185 +456,192 @@ class StokBantuanView extends GetView<StokBantuanController> {
final satuanController = TextEditingController(); final satuanController = TextEditingController();
final deskripsiController = TextEditingController(); final deskripsiController = TextEditingController();
String? selectedJenisBantuanId; String? selectedJenisBantuanId;
DateTime? tanggalMasuk = DateTime.now();
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime tanggalMasuk = DateTime.now();
DateTime? tanggalKadaluarsa; DateTime? tanggalKadaluarsa;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => StatefulBuilder(
title: const Text('Tambah Stok Bantuan'), builder: (context, setState) => AlertDialog(
content: Form( title: const Text('Tambah Stok Bantuan'),
key: formKey, content: Form(
child: SingleChildScrollView( key: formKey,
child: Column( child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
TextFormField( children: [
controller: namaController, TextFormField(
decoration: const InputDecoration( controller: namaController,
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Jenis Bantuan',
border: OutlineInputBorder(),
),
value: selectedJenisBantuanId,
hint: const Text('Pilih Jenis Bantuan'),
items: controller.daftarJenisBantuan
.map((jenis) => DropdownMenuItem<String>(
value: jenis['id'],
child: Text(jenis['nama'] ?? ''),
))
.toList(),
onChanged: (value) {
selectedJenisBantuanId = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jenis bantuan harus dipilih';
}
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( decoration: const InputDecoration(
labelText: 'Tanggal Masuk', labelText: 'Nama Bantuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
child: Text( validator: (value) {
DateFormatter.formatDate(tanggalMasuk), if (value == null || value.isEmpty) {
), return 'Nama bantuan tidak boleh kosong';
}
return null;
},
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), DropdownButtonFormField<String>(
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( decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa', labelText: 'Kategori Bantuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
child: Text( value: selectedJenisBantuanId,
DateFormatter.formatDate(tanggalKadaluarsa), hint: const Text('Pilih Kategori Bantuan'),
items: controller.daftarKategoriBantuan
.map((kategori) => DropdownMenuItem<String>(
value: kategori['id'],
child: Text(kategori['nama'] ?? ''),
))
.toList(),
onChanged: (value) {
selectedJenisBantuanId = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Kategori bantuan harus dipilih';
}
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,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDateTime(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) {
setState(() {
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,
kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
controller.addStok(stok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
), ),
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,
jenisBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: 'TERSEDIA',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
controller.addStok(stok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
), ),
); );
} }
@ -609,187 +653,199 @@ class StokBantuanView extends GetView<StokBantuanController> {
TextEditingController(text: stok.jumlah?.toString()); TextEditingController(text: stok.jumlah?.toString());
final satuanController = TextEditingController(text: stok.satuan); final satuanController = TextEditingController(text: stok.satuan);
final deskripsiController = TextEditingController(text: stok.deskripsi); final deskripsiController = TextEditingController(text: stok.deskripsi);
String? selectedJenisBantuanId = stok.jenisBantuanId; String? selectedJenisBantuanId = stok.kategoriBantuanId;
// Gunakan StatefulBuilder untuk memperbarui state dialog
DateTime? tanggalMasuk = stok.tanggalMasuk; DateTime? tanggalMasuk = stok.tanggalMasuk;
DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa; DateTime? tanggalKadaluarsa = stok.tanggalKadaluarsa;
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => StatefulBuilder(
title: const Text('Edit Stok Bantuan'), builder: (context, setState) => AlertDialog(
content: Form( title: const Text('Edit Stok Bantuan'),
key: formKey, content: Form(
child: SingleChildScrollView( key: formKey,
child: Column( child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
TextFormField( children: [
controller: namaController, TextFormField(
decoration: const InputDecoration( controller: namaController,
labelText: 'Nama Bantuan',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama bantuan tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Jenis Bantuan',
border: OutlineInputBorder(),
),
value: selectedJenisBantuanId,
hint: const Text('Pilih Jenis Bantuan'),
items: controller.daftarJenisBantuan
.map((jenis) => DropdownMenuItem<String>(
value: jenis['id'],
child: Text(jenis['nama'] ?? ''),
))
.toList(),
onChanged: (value) {
selectedJenisBantuanId = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Jenis bantuan harus dipilih';
}
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( decoration: const InputDecoration(
labelText: 'Tanggal Masuk', labelText: 'Nama Bantuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
child: Text( validator: (value) {
DateFormatter.formatDate(tanggalMasuk), if (value == null || value.isEmpty) {
), return 'Nama bantuan tidak boleh kosong';
}
return null;
},
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), DropdownButtonFormField<String>(
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( decoration: const InputDecoration(
labelText: 'Tanggal Kadaluarsa', labelText: 'Kategori Bantuan',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
child: Text( value: selectedJenisBantuanId,
DateFormatter.formatDate(tanggalKadaluarsa), hint: const Text('Pilih Kategori Bantuan'),
isExpanded: true,
items: controller.daftarKategoriBantuan
.map((kategori) => DropdownMenuItem<String>(
value: kategori['id'],
child: Text(
kategori['nama'] ?? '',
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
))
.toList(),
onChanged: (value) {
selectedJenisBantuanId = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Kategori bantuan harus dipilih';
}
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) {
setState(() {
tanggalMasuk = picked;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Tanggal Masuk',
border: OutlineInputBorder(),
),
child: Text(
DateFormatter.formatDateTime(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) {
setState(() {
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,
kategoriBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
createdAt: stok.createdAt,
updatedAt: DateTime.now(),
);
controller.updateStok(updatedStok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
), ),
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,
jenisBantuanId: selectedJenisBantuanId,
tanggalMasuk: tanggalMasuk,
tanggalKadaluarsa: tanggalKadaluarsa,
status: stok.status,
createdAt: stok.createdAt,
updatedAt: DateTime.now(),
);
controller.updateStok(updatedStok);
Navigator.pop(context);
}
},
child: const Text('Simpan'),
),
],
), ),
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart'; import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
@ -14,12 +15,19 @@ class _SplashViewState extends State<SplashView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_navigateToLogin(); _checkAuthAndNavigate();
} }
_navigateToLogin() async { _checkAuthAndNavigate() async {
// Tunggu 2 detik untuk menampilkan splash screen
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
Get.offAllNamed(Routes.login);
// Dapatkan AuthController dan periksa status autentikasi
final AuthController authController = Get.find<AuthController>();
await authController.checkAuthStatus();
// Navigasi akan ditangani oleh AuthController
// Tidak perlu navigasi manual di sini
} }
@override @override

View File

@ -9,6 +9,9 @@ class SupabaseService extends GetxService {
// Cache untuk profil pengguna // Cache untuk profil pengguna
Map<String, dynamic>? _cachedUserProfile; Map<String, dynamic>? _cachedUserProfile;
// Flag untuk menandai apakah sesi sudah diinisialisasi
bool _isSessionInitialized = false;
// Ganti dengan URL dan API key Supabase Anda // Ganti dengan URL dan API key Supabase Anda
static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL', static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL',
defaultValue: 'http://labulabs.net:8000'); defaultValue: 'http://labulabs.net:8000');
@ -17,13 +20,47 @@ class SupabaseService extends GetxService {
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzMxODYyODAwLAogICJleHAiOiAxODg5NjI5MjAwCn0.4IpwhwCVbfYXxb8JlZOLSBzCt6kQmypkvuso7N8Aicc'); 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzMxODYyODAwLAogICJleHAiOiAxODg5NjI5MjAwCn0.4IpwhwCVbfYXxb8JlZOLSBzCt6kQmypkvuso7N8Aicc');
Future<SupabaseService> init() async { Future<SupabaseService> init() async {
await Supabase.initialize( try {
url: supabaseUrl, await Supabase.initialize(
anonKey: supabaseKey, url: supabaseUrl,
); anonKey: supabaseKey,
debug: true, // Aktifkan debug untuk melihat log autentikasi
);
client = Supabase.instance.client; client = Supabase.instance.client;
return this;
// Tambahkan listener untuk perubahan autentikasi
client.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent event = data.event;
print('DEBUG: Auth state changed: $event');
if (event == AuthChangeEvent.signedIn) {
print('DEBUG: User signed in');
_isSessionInitialized = true;
} else if (event == AuthChangeEvent.signedOut) {
print('DEBUG: User signed out');
_cachedUserProfile = null;
_isSessionInitialized = false;
} else if (event == AuthChangeEvent.tokenRefreshed) {
print('DEBUG: Token refreshed');
_isSessionInitialized = true;
}
});
// Periksa apakah ada sesi yang aktif
final session = client.auth.currentSession;
if (session != null) {
print('DEBUG: Session aktif ditemukan saat inisialisasi');
_isSessionInitialized = true;
} else {
print('DEBUG: Tidak ada session aktif saat inisialisasi');
}
return this;
} catch (e) {
print('ERROR: Gagal inisialisasi Supabase: $e');
rethrow;
}
} }
// Metode untuk mendaftar pengguna baru // Metode untuk mendaftar pengguna baru
@ -37,23 +74,52 @@ class SupabaseService extends GetxService {
// Metode untuk login // Metode untuk login
Future<AuthResponse> signIn(String email, String password) async { Future<AuthResponse> signIn(String email, String password) async {
return await client.auth.signInWithPassword( final response = await client.auth.signInWithPassword(
email: email, email: email,
password: password, password: password,
); );
if (response.user != null) {
_isSessionInitialized = true;
print('DEBUG: Login berhasil, sesi diinisialisasi');
}
return response;
} }
// Metode untuk logout // Metode untuk logout
Future<void> signOut() async { Future<void> signOut() async {
_cachedUserProfile = null; // Hapus cache saat logout _cachedUserProfile = null; // Hapus cache saat logout
_isSessionInitialized = false;
await client.auth.signOut(); await client.auth.signOut();
print('DEBUG: Logout berhasil, sesi dihapus');
} }
// Metode untuk mendapatkan user saat ini // Metode untuk mendapatkan user saat ini
User? get currentUser => client.auth.currentUser; User? get currentUser => client.auth.currentUser;
// Metode untuk memeriksa apakah user sudah login // Metode untuk memeriksa apakah user sudah login
bool get isAuthenticated => currentUser != null; bool get isAuthenticated {
final user = currentUser;
final session = client.auth.currentSession;
if (user != null && session != null) {
// Periksa apakah token masih valid
final now = DateTime.now().millisecondsSinceEpoch / 1000;
final isValid = session.expiresAt != null && session.expiresAt! > now;
if (isValid) {
print('DEBUG: Sesi valid, user terautentikasi');
return true;
} else {
print('DEBUG: Sesi kedaluwarsa, user tidak terautentikasi');
return false;
}
}
print('DEBUG: Tidak ada user atau sesi, user tidak terautentikasi');
return false;
}
// Metode untuk mendapatkan profil pengguna // Metode untuk mendapatkan profil pengguna
Future<Map<String, dynamic>?> getUserProfile() async { Future<Map<String, dynamic>?> getUserProfile() async {
@ -270,7 +336,7 @@ class SupabaseService extends GetxService {
try { try {
final response = await client final response = await client
.from('stok_bantuan') .from('stok_bantuan')
.select('*, jenis_bantuan:jenis_bantuan_id(id, nama)'); .select('*, kategori_bantuan:kategori_bantuan_id(id, nama)');
return response; return response;
} catch (e) { } catch (e) {
@ -318,18 +384,23 @@ class SupabaseService extends GetxService {
} }
} }
Future<List<Map<String, dynamic>>?> getJenisBantuan() async { Future<List<Map<String, dynamic>>?> getKategoriBantuan() async {
try { try {
final response = await client.from('jenis_bantuan').select('*'); final response = await client.from('kategori_bantuan').select('*');
return response; return response;
} catch (e) { } catch (e) {
print('Error getting jenis bantuan: $e'); print('Error getting kategori bantuan: $e');
return null; return null;
} }
} }
Future<void> addStok(Map<String, dynamic> stokData) async { Future<void> addStok(Map<String, dynamic> stokData) async {
try { try {
print('stokData: $stokData');
// Hapus id dari stokData jika ada, biarkan Supabase yang menghasilkan id
if (stokData.containsKey('id')) {
stokData.remove('id');
}
await client.from('stok_bantuan').insert(stokData); await client.from('stok_bantuan').insert(stokData);
} catch (e) { } catch (e) {
print('Error adding stok: $e'); print('Error adding stok: $e');

View File

@ -14,6 +14,35 @@ class DateFormatter {
} }
} }
static String formatTime(DateTime? time,
{String format = 'HH:mm',
String locale = 'id_ID',
String defaultValue = '-'}) {
if (time == null) return defaultValue;
try {
return DateFormat(format, locale).format(time);
} catch (e) {
print('Error formatting time: $e');
return time
.toString()
.split(' ')[1]
.substring(0, 5); // Fallback to basic format
}
}
static String formatDateTime(DateTime? dateTime,
{String format = 'dd MMMM yyyy HH:mm',
String locale = 'id_ID',
String defaultValue = '-'}) {
if (dateTime == null) return defaultValue;
try {
return DateFormat(format, locale).format(dateTime);
} catch (e) {
print('Error formatting date time: $e');
return dateTime.toString().split('.')[0]; // Fallback to basic format
}
}
static String formatNumber(num? number, static String formatNumber(num? number,
{String locale = 'id_ID', String defaultValue = '0'}) { {String locale = 'id_ID', String defaultValue = '0'}) {
if (number == null) return defaultValue; if (number == null) return defaultValue;