645 lines
22 KiB
Dart
645 lines
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:SIBAYAM/admin/edit_rule_page.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:SIBAYAM/api_services/api_services.dart';
|
|
import 'tambah_rule_page.dart';
|
|
|
|
class RulePage extends StatefulWidget {
|
|
const RulePage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
_RulePageState createState() => _RulePageState();
|
|
}
|
|
|
|
class _RulePageState extends State<RulePage> {
|
|
List<Map<String, dynamic>> gejalaList = [];
|
|
List<Map<String, dynamic>> penyakitList = [];
|
|
List<Map<String, dynamic>> hamaList = [];
|
|
|
|
List<dynamic> rules = [];
|
|
List<dynamic> filteredRules = [];
|
|
bool isLoading = true;
|
|
|
|
// Search and filter variables
|
|
TextEditingController searchController = TextEditingController();
|
|
String selectedFilter = 'Semua'; // 'Semua', 'Penyakit', 'Hama'
|
|
|
|
// Pagination variables
|
|
int currentPage = 0;
|
|
int rowsPerPage = 10;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
fetchRules();
|
|
searchController.addListener(_onSearchChanged);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
searchController.removeListener(_onSearchChanged);
|
|
searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onSearchChanged() {
|
|
_filterRules();
|
|
}
|
|
|
|
void _filterRules() {
|
|
setState(() {
|
|
filteredRules = rules.where((rule) {
|
|
// Filter berdasarkan kategori
|
|
bool categoryMatch = true;
|
|
if (selectedFilter == 'Penyakit') {
|
|
categoryMatch = rule['id_penyakit'] != null;
|
|
} else if (selectedFilter == 'Hama') {
|
|
categoryMatch = rule['id_hama'] != null;
|
|
}
|
|
|
|
// Filter berdasarkan search text
|
|
bool searchMatch = true;
|
|
if (searchController.text.isNotEmpty) {
|
|
final searchText = searchController.text.toLowerCase();
|
|
final namaKategori = rule['id_penyakit'] != null
|
|
? (rule['nama_penyakit'] ?? '').toLowerCase()
|
|
: (rule['nama_hama'] ?? '').toLowerCase();
|
|
final namaGejala = (rule['nama_gejala'] ?? '').toLowerCase();
|
|
|
|
searchMatch = namaKategori.contains(searchText) ||
|
|
namaGejala.contains(searchText);
|
|
}
|
|
|
|
return categoryMatch && searchMatch;
|
|
}).toList();
|
|
|
|
// Reset ke halaman pertama setelah filter
|
|
currentPage = 0;
|
|
});
|
|
}
|
|
|
|
void fetchRules() async {
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
final apiService = ApiService();
|
|
|
|
try {
|
|
// Ambil semua data referensi
|
|
gejalaList = await apiService.getGejala();
|
|
penyakitList = await apiService.getPenyakit();
|
|
hamaList = await apiService.getHama();
|
|
|
|
// Ambil rules penyakit dan hama secara terpisah
|
|
final rulesPenyakit = await apiService.getRulesPenyakit();
|
|
final rulesHama = await apiService.getRulesHama();
|
|
|
|
// Gabungkan dan proses keduanya
|
|
final enrichedRules = [
|
|
// Mengolah rules penyakit
|
|
...rulesPenyakit.map((rule) {
|
|
final gejala = gejalaList.firstWhere(
|
|
(item) => item['id'] == rule['id_gejala'],
|
|
orElse: () => {'nama': 'Gejala tidak ditemukan'},
|
|
);
|
|
|
|
final penyakit = penyakitList.firstWhere(
|
|
(item) => item['id'] == rule['id_penyakit'],
|
|
orElse: () => {'nama': 'Penyakit tidak ditemukan'},
|
|
);
|
|
|
|
return {
|
|
'id': rule['id'],
|
|
'id_gejala': rule['id_gejala'],
|
|
'id_penyakit': rule['id_penyakit'],
|
|
'id_hama': null,
|
|
'nama_gejala': gejala['nama'],
|
|
'nama_penyakit': penyakit['nama'],
|
|
'nama_hama': null,
|
|
'nilai_pakar': rule['nilai_pakar'],
|
|
'type': 'Penyakit',
|
|
};
|
|
}),
|
|
// Mengolah rules hama
|
|
...rulesHama.map((rule) {
|
|
final gejala = gejalaList.firstWhere(
|
|
(item) => item['id'] == rule['id_gejala'],
|
|
orElse: () => {'nama': 'Gejala tidak ditemukan'},
|
|
);
|
|
|
|
final hama = hamaList.firstWhere(
|
|
(item) => item['id'] == rule['id_hama'],
|
|
orElse: () => {'nama': 'Hama tidak ditemukan'},
|
|
);
|
|
|
|
return {
|
|
'id': rule['id'],
|
|
'id_gejala': rule['id_gejala'],
|
|
'id_penyakit': null,
|
|
'id_hama': rule['id_hama'],
|
|
'nama_gejala': gejala['nama'],
|
|
'nama_penyakit': null,
|
|
'nama_hama': hama['nama'],
|
|
'nilai_pakar': rule['nilai_pakar'],
|
|
'type': 'Hama',
|
|
};
|
|
}),
|
|
];
|
|
|
|
setState(() {
|
|
rules = enrichedRules;
|
|
filteredRules = enrichedRules;
|
|
});
|
|
} catch (e) {
|
|
print('Terjadi kesalahan saat memuat data: $e');
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal memuat data: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> deleteRule(Map<String, dynamic> rule) async {
|
|
// Tampilkan dialog konfirmasi
|
|
bool? confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Konfirmasi Hapus'),
|
|
content: Text('Apakah Anda yakin ingin menghapus rule ini?'),
|
|
actions: [
|
|
TextButton(
|
|
child: Text('Batal'),
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
),
|
|
TextButton(
|
|
child: Text('Hapus', style: TextStyle(color: Colors.red)),
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
if (confirm != true) return;
|
|
|
|
try {
|
|
http.Response res;
|
|
|
|
// Tentukan fungsi delete berdasarkan isi rule
|
|
if (rule['id_hama'] != null) {
|
|
res = await ApiService.deleteRuleHama(rule['id']);
|
|
} else if (rule['id_penyakit'] != null) {
|
|
res = await ApiService.deleteRulePenyakit(rule['id']);
|
|
} else {
|
|
throw Exception("Data rule tidak valid");
|
|
}
|
|
|
|
if (res.statusCode == 200) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Rule berhasil dihapus"),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
fetchRules(); // Refresh data setelah delete
|
|
} else {
|
|
throw Exception("Gagal menghapus rule");
|
|
}
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Terjadi kesalahan saat menghapus: $e"),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get paginated data
|
|
List<dynamic> get paginatedRules {
|
|
final startIndex = currentPage * rowsPerPage;
|
|
final endIndex = startIndex + rowsPerPage > filteredRules.length
|
|
? filteredRules.length
|
|
: startIndex + rowsPerPage;
|
|
|
|
if (startIndex >= filteredRules.length) {
|
|
return [];
|
|
}
|
|
|
|
return filteredRules.sublist(startIndex, endIndex);
|
|
}
|
|
|
|
Widget _buildSearchAndFilter() {
|
|
return Card(
|
|
elevation: 2,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: TextField(
|
|
controller: searchController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Cari berdasarkan nama penyakit, hama, atau gejala...',
|
|
prefixIcon: Icon(Icons.search),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 16),
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
value: selectedFilter,
|
|
decoration: InputDecoration(
|
|
labelText: 'Filter Kategori',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
),
|
|
items: ['Semua', 'Penyakit', 'Hama'].map((String value) {
|
|
return DropdownMenuItem<String>(
|
|
value: value,
|
|
child: Text(value),
|
|
);
|
|
}).toList(),
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
selectedFilter = newValue!;
|
|
_filterRules();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Total: ${filteredRules.length} rule(s)',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => TambahRulePage(
|
|
isEditing: false,
|
|
isEditingHama: true,
|
|
selectedRuleIds: [],
|
|
selectedGejalaIds: [],
|
|
nilaiPakarList: [],
|
|
selectedHamaId: null,
|
|
selectedPenyakitId: null,
|
|
showHamaOnly: true,
|
|
),
|
|
),
|
|
).then((_) => fetchRules());
|
|
},
|
|
icon: Icon(Icons.bug_report, size: 16),
|
|
label: Text("Rule Hama", style: TextStyle(fontSize: 12)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
minimumSize: Size(0, 36),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => TambahRulePage(
|
|
isEditing: false,
|
|
isEditingHama: false,
|
|
selectedRuleIds: [],
|
|
selectedGejalaIds: [],
|
|
nilaiPakarList: [],
|
|
selectedHamaId: null,
|
|
selectedPenyakitId: null,
|
|
showPenyakitOnly: true,
|
|
),
|
|
),
|
|
).then((_) => fetchRules());
|
|
},
|
|
icon: Icon(Icons.healing, size: 16),
|
|
label: Text("Rule Penyakit", style: TextStyle(fontSize: 12)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
minimumSize: Size(0, 36),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDataList() {
|
|
if (paginatedRules.isEmpty) {
|
|
return Container(
|
|
padding: EdgeInsets.all(32),
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.inbox, size: 64, color: Colors.grey[400]),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Tidak ada data rule',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
if (searchController.text.isNotEmpty || selectedFilter != 'Semua')
|
|
TextButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
searchController.clear();
|
|
selectedFilter = 'Semua';
|
|
_filterRules();
|
|
});
|
|
},
|
|
child: Text('Reset Filter'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: paginatedRules.length,
|
|
itemBuilder: (context, index) {
|
|
final rule = paginatedRules[index];
|
|
|
|
final namaKategori = rule['id_penyakit'] != null
|
|
? rule['nama_penyakit'] ?? '-'
|
|
: rule['nama_hama'] ?? '-';
|
|
|
|
final kategori = rule['type'] ?? 'Unknown';
|
|
final isHama = rule['id_hama'] != null;
|
|
|
|
return Container(
|
|
margin: EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
children: [
|
|
// Card dengan data rule
|
|
Expanded(
|
|
child: Card(
|
|
elevation: 2,
|
|
child: InkWell(
|
|
onTap: () => _navigateToEdit(rule),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Kategori badge dan nama
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isHama ? Colors.green[100] : Colors.blue[100],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
kategori,
|
|
style: TextStyle(
|
|
color: isHama ? Colors.green[800] : Colors.blue[800],
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
namaKategori,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 8),
|
|
// Gejala
|
|
Text(
|
|
'Gejala: ${rule['nama_gejala'] ?? '-'}',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[700],
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 4),
|
|
// Nilai pakar
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'Nilai Pakar: ',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Color(0xFF9DC08D),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
rule['nilai_pakar']?.toString() ?? '-',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFFFFFFFF),
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
// Button hapus di luar card
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.red,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: IconButton(
|
|
icon: Icon(Icons.delete, color: Colors.white),
|
|
onPressed: () => deleteRule(rule),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _navigateToEdit(Map<String, dynamic> rule) {
|
|
if (rule != null &&
|
|
rule['id'] != null &&
|
|
rule['id_gejala'] != null &&
|
|
rule['nilai_pakar'] != null) {
|
|
final bool editingHama = rule['id_hama'] != null;
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => EditRulePage(
|
|
isEditing: true,
|
|
isEditingHama: editingHama,
|
|
selectedRuleIds: [rule['id'] as int],
|
|
selectedGejalaIds: [rule['id_gejala'] as int],
|
|
nilaiPakarList: [(rule['nilai_pakar'] as num).toDouble()],
|
|
selectedHamaId: rule['id_hama'] as int?,
|
|
selectedPenyakitId: rule['id_penyakit'] as int?,
|
|
showHamaOnly: editingHama,
|
|
showPenyakitOnly: !editingHama,
|
|
),
|
|
),
|
|
).then((_) => fetchRules());
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Data rule tidak lengkap"),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _buildPaginationControls() {
|
|
final totalPages = (filteredRules.length / rowsPerPage).ceil();
|
|
|
|
if (totalPages <= 1) return SizedBox.shrink();
|
|
|
|
return Card(
|
|
elevation: 1,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Menampilkan ${(currentPage * rowsPerPage) + 1} - ${((currentPage + 1) * rowsPerPage > filteredRules.length) ? filteredRules.length : (currentPage + 1) * rowsPerPage} dari ${filteredRules.length}',
|
|
style: TextStyle(color: Colors.grey[600]),
|
|
),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(Icons.first_page),
|
|
onPressed: currentPage > 0
|
|
? () => setState(() => currentPage = 0)
|
|
: null,
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.chevron_left),
|
|
onPressed: currentPage > 0
|
|
? () => setState(() => currentPage--)
|
|
: null,
|
|
),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
child: Text('${currentPage + 1} / $totalPages'),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.chevron_right),
|
|
onPressed: (currentPage + 1) * rowsPerPage < filteredRules.length
|
|
? () => setState(() => currentPage++)
|
|
: null,
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.last_page),
|
|
onPressed: (currentPage + 1) * rowsPerPage < filteredRules.length
|
|
? () => setState(() => currentPage = totalPages - 1)
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Halaman Aturan'),
|
|
backgroundColor: Color(0xFF9DC08D),
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.refresh),
|
|
onPressed: fetchRules,
|
|
tooltip: 'Refresh Data',
|
|
),
|
|
],
|
|
),
|
|
body: isLoading
|
|
? Center(child: CircularProgressIndicator())
|
|
: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
_buildSearchAndFilter(),
|
|
SizedBox(height: 16),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
Expanded(child: _buildDataList()),
|
|
SizedBox(height: 8),
|
|
_buildPaginationControls(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |