diff --git a/lib/app/data/models/stok_bantuan_model.dart b/lib/app/data/models/stok_bantuan_model.dart index 160ce32..b4d37b3 100644 --- a/lib/app/data/models/stok_bantuan_model.dart +++ b/lib/app/data/models/stok_bantuan_model.dart @@ -3,14 +3,11 @@ import 'dart:convert'; class StokBantuanModel { final String? id; final String? nama; - final String? bentukBantuanId; - final String? sumberBantuanId; - final String? jenisBantuanId; - final Map? jenisBantuan; + final String? kategoriBantuanId; + final Map? kategoriBantuan; final double? jumlah; final String? satuan; final String? deskripsi; - final String? status; final DateTime? tanggalMasuk; final DateTime? tanggalKadaluarsa; final DateTime? createdAt; @@ -19,14 +16,11 @@ class StokBantuanModel { StokBantuanModel({ this.id, this.nama, - this.bentukBantuanId, - this.sumberBantuanId, - this.jenisBantuanId, - this.jenisBantuan, + this.kategoriBantuanId, + this.kategoriBantuan, this.jumlah, this.satuan, this.deskripsi, - this.status, this.tanggalMasuk, this.tanggalKadaluarsa, this.createdAt, @@ -42,14 +36,11 @@ class StokBantuanModel { StokBantuanModel( id: json["id"], nama: json["nama"], - bentukBantuanId: json["bentuk_bantuan_id"], - sumberBantuanId: json["sumber_bantuan_id"], - jenisBantuanId: json["jenis_bantuan_id"], - jenisBantuan: json["jenis_bantuan"], + kategoriBantuanId: json["kategori_bantuan_id"], + kategoriBantuan: json["kategori_bantuan"], jumlah: json["jumlah"] != null ? json["jumlah"].toDouble() : 0.0, satuan: json["satuan"], deskripsi: json["deskripsi"], - status: json["status"], tanggalMasuk: json["tanggal_masuk"] != null ? DateTime.parse(json["tanggal_masuk"]) : null, @@ -64,19 +55,24 @@ class StokBantuanModel { : null, ); - Map toJson() => { - "id": id, - "nama": nama, - "bentuk_bantuan_id": bentukBantuanId, - "sumber_bantuan_id": sumberBantuanId, - "jenis_bantuan_id": jenisBantuanId, - "jumlah": jumlah, - "satuan": satuan, - "deskripsi": deskripsi, - "status": status, - "tanggal_masuk": tanggalMasuk?.toIso8601String(), - "tanggal_kadaluarsa": tanggalKadaluarsa?.toIso8601String(), - "created_at": createdAt?.toIso8601String(), - "updated_at": updatedAt?.toIso8601String(), - }; + Map toJson() { + final Map data = { + "nama": nama, + "kategori_bantuan_id": kategoriBantuanId, + "jumlah": jumlah, + "satuan": satuan, + "deskripsi": deskripsi, + "tanggal_masuk": tanggalMasuk?.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; + } } diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart index 073b0f4..dda51a8 100644 --- a/lib/app/modules/auth/controllers/auth_controller.dart +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -116,9 +116,14 @@ class AuthController extends GetxController { final targetRoute = _getTargetRouteForRole(role); 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'); 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 { 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) { switch (role) { case 'WARGA': diff --git a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart index b70ad9d..8f80a42 100644 --- a/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart +++ b/lib/app/modules/petugas_desa/bindings/petugas_desa_binding.dart @@ -17,45 +17,54 @@ class PetugasDesaBinding extends Bindings { Get.put(AuthController(), permanent: true); } - // Main controller - Get.lazyPut( - () => PetugasDesaController(), - fenix: true, - ); + // Main controller - gunakan put dengan permanent untuk controller utama + if (!Get.isRegistered()) { + Get.put(PetugasDesaController(), permanent: true); + } else { + // Jika sudah terdaftar, gunakan find untuk mendapatkan instance yang ada + Get.find(); + } // Dashboard controller Get.lazyPut( () => PetugasDesaDashboardController(), + fenix: true, ); // Jadwal penyaluran controller Get.lazyPut( () => JadwalPenyaluranController(), + fenix: true, ); // Stok bantuan controller Get.lazyPut( () => StokBantuanController(), + fenix: true, ); // Penitipan bantuan controller Get.lazyPut( () => PenitipanBantuanController(), + fenix: true, ); // Pengaduan controller Get.lazyPut( () => PengaduanController(), + fenix: true, ); // Penerima bantuan controller Get.lazyPut( () => PenerimaBantuanController(), + fenix: true, ); // Laporan controller Get.lazyPut( () => LaporanController(), + fenix: true, ); } } diff --git a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart index 9c98d28..2366c96 100644 --- a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart +++ b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart @@ -148,7 +148,7 @@ class JadwalSectionWidget extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'Jenis Bantuan: ${jadwalData['jenis_bantuan'] ?? ''}', + 'Kategori Bantuan: ${jadwalData['kategori_bantuan'] ?? ''}', style: textTheme.bodyMedium, ), const SizedBox(height: 4), diff --git a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart index 46d6263..86167c9 100644 --- a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart +++ b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart @@ -181,7 +181,7 @@ class PermintaanPenjadwalanSummaryWidget extends StatelessWidget { ), const SizedBox(height: 4), Text( - 'Jenis: ${permintaanData['jenis_bantuan'] ?? ''}', + 'Kategori: ${permintaanData['kategori_bantuan'] ?? ''}', style: textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart index 1f06932..03c750e 100644 --- a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -17,8 +17,8 @@ class StokBantuanController extends GetxController { final RxDouble stokMasuk = 0.0.obs; final RxDouble stokKeluar = 0.0.obs; - // Data untuk jenis bantuan - final RxList> daftarJenisBantuan = + // Data untuk kategori bantuan + final RxList> daftarKategoriBantuan = >[].obs; // Controller untuk pencarian @@ -31,7 +31,7 @@ class StokBantuanController extends GetxController { void onInit() { super.onInit(); loadStokBantuanData(); - loadJenisBantuanData(); + loadKategoriBantuanData(); // Listener untuk pencarian searchController.addListener(() { @@ -74,14 +74,14 @@ class StokBantuanController extends GetxController { } } - Future loadJenisBantuanData() async { + Future loadKategoriBantuanData() async { try { - final jenisBantuanData = await _supabaseService.getJenisBantuan(); - if (jenisBantuanData != null) { - daftarJenisBantuan.value = jenisBantuanData; + final kategoriBantuanData = await _supabaseService.getKategoriBantuan(); + if (kategoriBantuanData != null) { + daftarKategoriBantuan.value = kategoriBantuanData; } } 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; try { await loadStokBantuanData(); - await loadJenisBantuanData(); + await loadKategoriBantuanData(); } finally { isLoading.value = false; } @@ -194,4 +194,18 @@ class StokBantuanController extends GetxController { .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; + } } diff --git a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart index cc7c3f2..8de8b2e 100644 --- a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart @@ -54,8 +54,8 @@ class PelaksanaanPenyaluranView extends GetView { const SizedBox(height: 16), _buildInfoItem(context, icon: Icons.category, - label: 'Jenis Bantuan', - value: jadwal['jenis_bantuan'] ?? '-'), + label: 'Kategori Bantuan', + value: jadwal['kategori_bantuan'] ?? '-'), const SizedBox(height: 8), _buildInfoItem(context, icon: Icons.calendar_today, diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index 78f52e5..b314816 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -169,7 +169,7 @@ class PenitipanView extends GetView { { 'id': '1', 'donatur': 'PT Sejahtera Abadi', - 'jenis_bantuan': 'Sembako', + 'kategori_bantuan': 'Sembako', 'jumlah': '500 kg', 'tanggal_pengajuan': '15 April 2023', 'status': 'Menunggu', @@ -177,7 +177,7 @@ class PenitipanView extends GetView { { 'id': '2', 'donatur': 'Yayasan Peduli Sesama', - 'jenis_bantuan': 'Pakaian', + 'kategori_bantuan': 'Pakaian', 'jumlah': '200 pcs', 'tanggal_pengajuan': '14 April 2023', 'status': 'Terverifikasi', @@ -185,7 +185,7 @@ class PenitipanView extends GetView { { 'id': '3', 'donatur': 'Bank BRI', - 'jenis_bantuan': 'Beras', + 'kategori_bantuan': 'Beras', 'jumlah': '300 kg', 'tanggal_pengajuan': '13 April 2023', 'status': 'Terverifikasi', @@ -193,7 +193,7 @@ class PenitipanView extends GetView { { 'id': '4', 'donatur': 'Komunitas Peduli', - 'jenis_bantuan': 'Alat Tulis', + 'kategori_bantuan': 'Alat Tulis', 'jumlah': '100 set', 'tanggal_pengajuan': '12 April 2023', 'status': 'Ditolak', @@ -304,8 +304,8 @@ class PenitipanView extends GetView { child: _buildItemDetail( context, icon: Icons.category, - label: 'Jenis Bantuan', - value: item['jenis_bantuan'] ?? '', + label: 'Kategori Bantuan', + value: item['kategori_bantuan'] ?? '', ), ), Expanded( diff --git a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart index 7acf7f7..8b501d0 100644 --- a/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart +++ b/lib/app/modules/petugas_desa/views/stok_bantuan_view.dart @@ -23,7 +23,7 @@ class StokBantuanView extends GetView { _showAddStokDialog(context); }, backgroundColor: AppTheme.primaryColor, - child: const Icon(Icons.add), + child: const Icon(Icons.add, color: Colors.white), ), ); } @@ -79,7 +79,7 @@ class StokBantuanView extends GetView { child: _buildSummaryItem( context, icon: Icons.inventory_2_outlined, - title: 'Total Stok', + title: 'Stok Tersedia', value: DateFormatter.formatNumber(controller.totalStok.value), ), ), @@ -87,7 +87,7 @@ class StokBantuanView extends GetView { child: _buildSummaryItem( context, icon: Icons.input, - title: 'Masuk', + title: 'Total Masuk', value: DateFormatter.formatNumber(controller.stokMasuk.value), ), ), @@ -95,13 +95,48 @@ class StokBantuanView extends GetView { child: _buildSummaryItem( context, icon: Icons.output, - title: 'Keluar', + title: 'Total Keluar', 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 { required IconData icon, required String title, required String value, + Color? valueColor, }) { return Column( children: [ @@ -132,7 +168,7 @@ class StokBantuanView extends GetView { value, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, - color: Colors.white, + color: valueColor ?? Colors.white, ), ), const SizedBox(height: 4), @@ -276,9 +312,10 @@ class StokBantuanView extends GetView { borderRadius: BorderRadius.circular(8), ), child: Text( - item.jenisBantuan != null - ? (item.jenisBantuan!['nama'] ?? 'Tidak Ada Jenis') - : 'Tidak Ada Jenis', + item.kategoriBantuan != null + ? (item.kategoriBantuan!['nama'] ?? + 'Tidak Ada Kategori') + : 'Tidak Ada Kategori', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.primaryColor, fontWeight: FontWeight.bold, @@ -314,7 +351,7 @@ class StokBantuanView extends GetView { context, icon: Icons.calendar_today, label: 'Tanggal Masuk', - value: DateFormatter.formatDate(item.tanggalMasuk), + value: DateFormatter.formatDateTime(item.tanggalMasuk), ), ), ], @@ -335,7 +372,7 @@ class StokBantuanView extends GetView { context, icon: Icons.access_time, label: 'Terakhir Diperbarui', - value: DateFormatter.formatDate(item.updatedAt), + value: DateFormatter.formatDateTime(item.updatedAt), ), ), ], @@ -419,185 +456,192 @@ class StokBantuanView extends GetView { final satuanController = TextEditingController(); final deskripsiController = TextEditingController(); String? selectedJenisBantuanId; - DateTime? tanggalMasuk = DateTime.now(); + + // Gunakan StatefulBuilder untuk memperbarui state dialog + 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), - DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Jenis Bantuan', - border: OutlineInputBorder(), - ), - value: selectedJenisBantuanId, - hint: const Text('Pilih Jenis Bantuan'), - items: controller.daftarJenisBantuan - .map((jenis) => DropdownMenuItem( - 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( + builder: (context) => StatefulBuilder( + builder: (context, setState) => 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: 'Tanggal Masuk', + labelText: 'Nama Bantuan', border: OutlineInputBorder(), ), - child: Text( - DateFormatter.formatDate(tanggalMasuk), - ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama bantuan tidak boleh kosong'; + } + return null; + }, ), - ), - 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( + const SizedBox(height: 16), + DropdownButtonFormField( decoration: const InputDecoration( - labelText: 'Tanggal Kadaluarsa', + labelText: 'Kategori Bantuan', border: OutlineInputBorder(), ), - child: Text( - DateFormatter.formatDate(tanggalKadaluarsa), + value: selectedJenisBantuanId, + hint: const Text('Pilih Kategori Bantuan'), + items: controller.daftarKategoriBantuan + .map((kategori) => DropdownMenuItem( + 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 { TextEditingController(text: stok.jumlah?.toString()); final satuanController = TextEditingController(text: stok.satuan); 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? 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), - DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Jenis Bantuan', - border: OutlineInputBorder(), - ), - value: selectedJenisBantuanId, - hint: const Text('Pilih Jenis Bantuan'), - items: controller.daftarJenisBantuan - .map((jenis) => DropdownMenuItem( - 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( + builder: (context) => StatefulBuilder( + builder: (context, setState) => 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: 'Tanggal Masuk', + labelText: 'Nama Bantuan', border: OutlineInputBorder(), ), - child: Text( - DateFormatter.formatDate(tanggalMasuk), - ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama bantuan tidak boleh kosong'; + } + return null; + }, ), - ), - 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( + const SizedBox(height: 16), + DropdownButtonFormField( decoration: const InputDecoration( - labelText: 'Tanggal Kadaluarsa', + labelText: 'Kategori Bantuan', border: OutlineInputBorder(), ), - child: Text( - DateFormatter.formatDate(tanggalKadaluarsa), + value: selectedJenisBantuanId, + hint: const Text('Pilih Kategori Bantuan'), + isExpanded: true, + items: controller.daftarKategoriBantuan + .map((kategori) => DropdownMenuItem( + 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'), - ), - ], ), ); } diff --git a/lib/app/modules/splash/views/splash_view.dart b/lib/app/modules/splash/views/splash_view.dart index fff53ca..3071fa5 100644 --- a/lib/app/modules/splash/views/splash_view.dart +++ b/lib/app/modules/splash/views/splash_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.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/theme/app_theme.dart'; @@ -14,12 +15,19 @@ class _SplashViewState extends State { @override void initState() { super.initState(); - _navigateToLogin(); + _checkAuthAndNavigate(); } - _navigateToLogin() async { + _checkAuthAndNavigate() async { + // Tunggu 2 detik untuk menampilkan splash screen await Future.delayed(const Duration(seconds: 2)); - Get.offAllNamed(Routes.login); + + // Dapatkan AuthController dan periksa status autentikasi + final AuthController authController = Get.find(); + await authController.checkAuthStatus(); + + // Navigasi akan ditangani oleh AuthController + // Tidak perlu navigasi manual di sini } @override diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index bcaefb8..5e92ee6 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -9,6 +9,9 @@ class SupabaseService extends GetxService { // Cache untuk profil pengguna Map? _cachedUserProfile; + // Flag untuk menandai apakah sesi sudah diinisialisasi + bool _isSessionInitialized = false; + // Ganti dengan URL dan API key Supabase Anda static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL', defaultValue: 'http://labulabs.net:8000'); @@ -17,13 +20,47 @@ class SupabaseService extends GetxService { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzMxODYyODAwLAogICJleHAiOiAxODg5NjI5MjAwCn0.4IpwhwCVbfYXxb8JlZOLSBzCt6kQmypkvuso7N8Aicc'); Future init() async { - await Supabase.initialize( - url: supabaseUrl, - anonKey: supabaseKey, - ); + try { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: true, // Aktifkan debug untuk melihat log autentikasi + ); - client = Supabase.instance.client; - return this; + client = Supabase.instance.client; + + // 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 @@ -37,23 +74,52 @@ class SupabaseService extends GetxService { // Metode untuk login Future signIn(String email, String password) async { - return await client.auth.signInWithPassword( + final response = await client.auth.signInWithPassword( email: email, password: password, ); + + if (response.user != null) { + _isSessionInitialized = true; + print('DEBUG: Login berhasil, sesi diinisialisasi'); + } + + return response; } // Metode untuk logout Future signOut() async { _cachedUserProfile = null; // Hapus cache saat logout + _isSessionInitialized = false; await client.auth.signOut(); + print('DEBUG: Logout berhasil, sesi dihapus'); } // Metode untuk mendapatkan user saat ini User? get currentUser => client.auth.currentUser; // 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 Future?> getUserProfile() async { @@ -270,7 +336,7 @@ class SupabaseService extends GetxService { try { final response = await client .from('stok_bantuan') - .select('*, jenis_bantuan:jenis_bantuan_id(id, nama)'); + .select('*, kategori_bantuan:kategori_bantuan_id(id, nama)'); return response; } catch (e) { @@ -318,18 +384,23 @@ class SupabaseService extends GetxService { } } - Future>?> getJenisBantuan() async { + Future>?> getKategoriBantuan() async { try { - final response = await client.from('jenis_bantuan').select('*'); + final response = await client.from('kategori_bantuan').select('*'); return response; } catch (e) { - print('Error getting jenis bantuan: $e'); + print('Error getting kategori bantuan: $e'); return null; } } Future addStok(Map stokData) async { 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); } catch (e) { print('Error adding stok: $e'); diff --git a/lib/app/utils/date_formatter.dart b/lib/app/utils/date_formatter.dart index 6752e02..7ff7c00 100644 --- a/lib/app/utils/date_formatter.dart +++ b/lib/app/utils/date_formatter.dart @@ -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, {String locale = 'id_ID', String defaultValue = '0'}) { if (number == null) return defaultValue;