update layout admin for get data

This commit is contained in:
unknown 2025-06-14 23:11:05 +07:00
parent 213eb6273c
commit db83fb1c2f
15 changed files with 2569 additions and 1355 deletions

View File

@ -19,8 +19,6 @@ dotenv.config();
const app = express(); const app = express();
// Middlewares // Middlewares
app.use(express.json()); app.use(express.json());
app.use(cors()); app.use(cors());

View File

@ -11,7 +11,7 @@ const swaggerOptions = {
}, },
servers: [ servers: [
{ {
url: 'https://backend-sistem-pakar-diagnosa-penya.vercel.app', url: 'https://localhost:5000', // Development server URL
description: 'Production Server' description: 'Production Server'
}, },
], ],

View File

@ -11,9 +11,14 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> historiData = []; List<Map<String, dynamic>> historiData = [];
List<Map<String, dynamic>> groupedHistoriData = []; List<Map<String, dynamic>> groupedHistoriData = [];
List<Map<String, dynamic>> filteredHistoriData = []; // Data yang sudah difilter
bool isLoading = true; bool isLoading = true;
String? error; String? error;
// Search variables
TextEditingController searchController = TextEditingController();
String searchQuery = '';
// Pagination variables // Pagination variables
int _rowsPerPage = 10; int _rowsPerPage = 10;
int _currentPage = 0; int _currentPage = 0;
@ -24,6 +29,35 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
void initState() { void initState() {
super.initState(); super.initState();
_loadHistoriData(); _loadHistoriData();
searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
searchController.removeListener(_onSearchChanged);
searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
setState(() {
searchQuery = searchController.text.toLowerCase();
_filterData();
_updatePagination(0); // Reset ke halaman pertama saat search
});
}
void _filterData() {
if (searchQuery.isEmpty) {
filteredHistoriData = List.from(groupedHistoriData);
} else {
filteredHistoriData = groupedHistoriData.where((histori) {
final userName = (histori['userName'] ?? '').toString().toLowerCase();
final diagnosa = (histori['diagnosa'] ?? '').toString().toLowerCase();
return userName.contains(searchQuery) || diagnosa.contains(searchQuery);
}).toList();
}
} }
Future<void> _loadHistoriData() async { Future<void> _loadHistoriData() async {
@ -34,7 +68,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
}); });
// Dapatkan semua histori terlebih dahulu // Dapatkan semua histori terlebih dahulu
final allHistori = await apiService.getAllHistori(); // Kumpulkan semua userIds yang unik final allHistori = await apiService.getAllHistori();
// Kumpulkan semua userIds yang unik
Set<String> uniqueUserIds = allHistori Set<String> uniqueUserIds = allHistori
.where((histori) => histori['userId'] != null) .where((histori) => histori['userId'] != null)
.map((histori) => histori['userId'].toString()) .map((histori) => histori['userId'].toString())
@ -60,6 +96,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
setState(() { setState(() {
historiData = detailedHistori; // Simpan data asli jika perlu historiData = detailedHistori; // Simpan data asli jika perlu
groupedHistoriData = groupedData; // Data yang sudah dikelompokkan groupedHistoriData = groupedData; // Data yang sudah dikelompokkan
filteredHistoriData = List.from(groupedData); // Initialize filtered data
_updatePagination(0); // Set halaman pertama _updatePagination(0); // Set halaman pertama
isLoading = false; isLoading = false;
}); });
@ -104,8 +141,8 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
diagnosa = 'Tidak ada diagnosa'; diagnosa = 'Tidak ada diagnosa';
} }
// Ambil nama user dari kolom 'nama' atau 'name' (sesuaikan dengan struktur data Anda) // Ambil nama user dari kolom 'nama' atau 'name'
String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}'; String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}';
// Buat composite key: userId + waktu + diagnosa // Buat composite key: userId + waktu + diagnosa
String key = '${item['userId']}_${formattedTime}_$diagnosa'; String key = '${item['userId']}_${formattedTime}_$diagnosa';
@ -116,7 +153,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
groupedMap[key] = { groupedMap[key] = {
'userId': item['userId'], 'userId': item['userId'],
'userName': userName, // Menampilkan nama user, bukan ID 'userName': userName,
'diagnosa': diagnosa, 'diagnosa': diagnosa,
'tanggal_diagnosa': item['tanggal_diagnosa'], 'tanggal_diagnosa': item['tanggal_diagnosa'],
'tanggal_display': displayDate, 'tanggal_display': displayDate,
@ -124,7 +161,8 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
'hasil': item['hasil'], 'hasil': item['hasil'],
'penyakit_nama': item['penyakit_nama'], 'penyakit_nama': item['penyakit_nama'],
'hama_nama': item['hama_nama'], 'hama_nama': item['hama_nama'],
'sortTime': dateTime.millisecondsSinceEpoch, // untuk pengurutan 'sortTime': dateTime.millisecondsSinceEpoch,
'detailData': [], // Menyimpan semua item detail untuk halaman detail
}; };
} }
@ -133,6 +171,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
!groupedMap[key]!['gejala'].contains(item['gejala_nama'])) { !groupedMap[key]!['gejala'].contains(item['gejala_nama'])) {
groupedMap[key]!['gejala'].add(item['gejala_nama']); groupedMap[key]!['gejala'].add(item['gejala_nama']);
} }
// Simpan data detail untuk halaman detail
groupedMap[key]!['detailData'].add(item);
} }
// Konversi map ke list dan urutkan berdasarkan waktu terbaru // Konversi map ke list dan urutkan berdasarkan waktu terbaru
@ -147,22 +188,32 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
// Update pagination // Update pagination
void _updatePagination(int page) { void _updatePagination(int page) {
_currentPage = page; _currentPage = page;
_totalPages = (groupedHistoriData.length / _rowsPerPage).ceil(); _totalPages = (filteredHistoriData.length / _rowsPerPage).ceil();
int startIndex = page * _rowsPerPage; int startIndex = page * _rowsPerPage;
int endIndex = (page + 1) * _rowsPerPage; int endIndex = (page + 1) * _rowsPerPage;
if (endIndex > groupedHistoriData.length) { if (endIndex > filteredHistoriData.length) {
endIndex = groupedHistoriData.length; endIndex = filteredHistoriData.length;
} }
if (startIndex >= groupedHistoriData.length) { if (startIndex >= filteredHistoriData.length) {
_currentPageData = []; _currentPageData = [];
} else { } else {
_currentPageData = groupedHistoriData.sublist(startIndex, endIndex); _currentPageData = filteredHistoriData.sublist(startIndex, endIndex);
} }
} }
// Navigasi ke halaman detail
void _navigateToDetail(Map<String, dynamic> histori) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailHistoriPage(histori: histori),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -170,224 +221,477 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
title: Text('Riwayat Diagnosa'), title: Text('Riwayat Diagnosa'),
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
), ),
body: body: isLoading
isLoading ? Center(child: CircularProgressIndicator())
? Center(child: CircularProgressIndicator()) : error != null
: error != null ? Center(child: Text('Error: $error'))
? Center(child: Text('Error: $error')) : groupedHistoriData.isEmpty
: groupedHistoriData.isEmpty ? Center(child: Text('Tidak ada data riwayat diagnosa'))
? Center(child: Text('Tidak ada data riwayat diagnosa')) : Column(
: Column( children: [
children: [ // Search Bar
Expanded( Container(
child: SingleChildScrollView( margin: EdgeInsets.all(16),
scrollDirection: Axis.horizontal, child: TextField(
child: SingleChildScrollView( controller: searchController,
child: DataTable( decoration: InputDecoration(
columnSpacing: 20, hintText: 'Cari berdasarkan nama user atau diagnosa...',
headingRowColor: MaterialStateProperty.all( prefixIcon: Icon(
Color(0xFF9DC08D).withOpacity(0.3), Icons.search,
color: Color(0xFF9DC08D),
),
suffixIcon: searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFF9DC08D)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFF9DC08D), width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
Expanded(
child: filteredHistoriData.isEmpty && searchQuery.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'Tidak ada hasil untuk "${searchController.text}"',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text(
'Coba gunakan kata kunci yang berbeda',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
), ),
columns: [ )
DataColumn( : ListView.builder(
label: Text( itemCount: _currentPageData.length,
'Nama User', itemBuilder: (context, index) {
style: TextStyle(fontWeight: FontWeight.bold), final histori = _currentPageData[index];
),
), return Container(
DataColumn( margin: EdgeInsets.only(bottom: 12, left: 16, right: 16),
label: Text( child: Row(
'Gejala', children: [
style: TextStyle(fontWeight: FontWeight.bold), // Card dengan informasi histori
), Expanded(
), child: Card(
DataColumn( elevation: 2,
label: Text( child: InkWell(
'Diagnosa', onTap: () => _navigateToDetail(histori),
style: TextStyle(fontWeight: FontWeight.bold), child: Container(
), padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
), child: Column(
DataColumn( crossAxisAlignment: CrossAxisAlignment.start,
label: Text( children: [
'Hasil', // Nama User
style: TextStyle(fontWeight: FontWeight.bold), Text(
), histori['userName'] ?? 'User tidak ditemukan',
), style: TextStyle(
DataColumn( fontSize: 16,
label: Text( fontWeight: FontWeight.w600,
'Tanggal', ),
style: TextStyle(fontWeight: FontWeight.bold), ),
), SizedBox(height: 8),
), // Diagnosa
], Text(
rows: histori['diagnosa'] ?? 'Tidak ada diagnosa',
_currentPageData.map((histori) { style: TextStyle(
// Gabungkan semua gejala menjadi satu string dengan koma fontSize: 14,
String gejalaText = "Tidak ada gejala"; fontWeight: FontWeight.w500,
if (histori['gejala'] != null && ),
(histori['gejala'] as List).isNotEmpty) { ),
gejalaText = (histori['gejala'] as List).join( SizedBox(height: 4),
', ', // Tanggal
); Text(
} histori['tanggal_display'] ?? '',
style: TextStyle(
return DataRow( fontSize: 12,
cells: [ color: Colors.grey[600],
DataCell(Text(histori['userName'] ?? 'User tidak ditemukan')), ),
DataCell( ),
Container( ],
constraints: BoxConstraints(
maxWidth: 200,
),
child: Tooltip(
message: gejalaText,
child: Text(
gejalaText,
overflow: TextOverflow.ellipsis,
), ),
), ),
), ),
), ),
DataCell( ),
Text( SizedBox(width: 8),
histori['diagnosa'] ?? // Button detail di luar card
'Tidak ada diagnosa', Container(
style: TextStyle( decoration: BoxDecoration(
color: Color(0xFF9DC08D),
fontWeight: FontWeight.w500, borderRadius: BorderRadius.circular(8),
),
),
), ),
DataCell( child: IconButton(
Text(_formatHasil(histori['hasil'])), icon: Icon(Icons.info_outline, color: Colors.white),
onPressed: () => _navigateToDetail(histori),
tooltip: 'Lihat Detail',
), ),
DataCell( ),
Text(histori['tanggal_display'] ?? ''), ],
), ),
], );
);
}).toList(),
),
),
),
), // Pagination controls
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.first_page, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed:
_currentPage > 0
? () {
setState(() {
_updatePagination(0);
});
}
: null,
),
IconButton(
icon: Icon(Icons.chevron_left, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed:
_currentPage > 0
? () {
setState(() {
_updatePagination(_currentPage - 1);
});
}
: null,
),
SizedBox(width: 8),
Text(
'${_currentPage + 1} / $_totalPages',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8),
IconButton(
icon: Icon(Icons.chevron_right, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed:
_currentPage < _totalPages - 1
? () {
setState(() {
_updatePagination(_currentPage + 1);
});
}
: null,
),
IconButton(
icon: Icon(Icons.last_page, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed:
_currentPage < _totalPages - 1
? () {
setState(() {
_updatePagination(_totalPages - 1);
});
}
: null,
),
],
),
), // Rows per page selector
Container(
padding: EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Rows per page: ',
style: TextStyle(fontSize: 12),
),
DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
menuMaxHeight: 200,
items:
[10, 20, 50, 100].map((value) {
return DropdownMenuItem<int>(
value: value,
child: Text('$value'),
);
}).toList(),
onChanged: (value) {
setState(() {
_rowsPerPage = value!;
_updatePagination(
0,
); // Kembali ke halaman pertama
});
}, },
), ),
], ),
),
// Pagination controls
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.first_page, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed: _currentPage > 0
? () {
setState(() {
_updatePagination(0);
});
}
: null,
),
IconButton(
icon: Icon(Icons.chevron_left, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed: _currentPage > 0
? () {
setState(() {
_updatePagination(_currentPage - 1);
});
}
: null,
),
SizedBox(width: 8),
Text(
'${_currentPage + 1} / $_totalPages',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8),
IconButton(
icon: Icon(Icons.chevron_right, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed: _currentPage < _totalPages - 1
? () {
setState(() {
_updatePagination(_currentPage + 1);
});
}
: null,
),
IconButton(
icon: Icon(Icons.last_page, size: 18),
padding: EdgeInsets.all(4),
constraints: BoxConstraints(
minWidth: 32,
minHeight: 32,
),
onPressed: _currentPage < _totalPages - 1
? () {
setState(() {
_updatePagination(_totalPages - 1);
});
}
: null,
),
],
), ),
], ),
// Rows per page selector
Container(
padding: EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Rows per page: ',
style: TextStyle(fontSize: 12),
),
DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
menuMaxHeight: 200,
items: [10, 20, 50, 100].map((value) {
return DropdownMenuItem<int>(
value: value,
child: Text('$value'),
);
}).toList(),
onChanged: (value) {
setState(() {
_rowsPerPage = value!;
_updatePagination(0);
});
},
),
],
),
),
],
),
);
}
Color _getDiagnosaColor(Map<String, dynamic> histori) {
if (histori['penyakit_nama'] != null) {
return Colors.red[700]!;
} else if (histori['hama_nama'] != null) {
return Colors.amber[800]!;
}
return Colors.black;
}
String _formatHasil(dynamic hasil) {
if (hasil == null) return '0%';
double hasilValue = double.tryParse(hasil.toString()) ?? 0.0;
return '${(hasilValue * 100).toStringAsFixed(2)}%';
}
}
// Halaman Detail Histori
class DetailHistoriPage extends StatelessWidget {
final Map<String, dynamic> histori;
const DetailHistoriPage({Key? key, required this.histori}) : super(key: key);
@override
Widget build(BuildContext context) {
// Gabungkan semua gejala menjadi satu string
String gejalaText = "Tidak ada gejala";
if (histori['gejala'] != null && (histori['gejala'] as List).isNotEmpty) {
gejalaText = (histori['gejala'] as List).join(', ');
}
return Scaffold(
appBar: AppBar(
title: Text('Detail Riwayat Diagnosa'),
backgroundColor: Color(0xFF9DC08D),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Color(0xFF9DC08D).withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Informasi Diagnosa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF9DC08D),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 20),
// Nama User
_buildDetailRow(
'Nama User',
histori['userName'] ?? 'User tidak ditemukan',
Icons.person,
),
SizedBox(height: 16),
// Tanggal Diagnosa
_buildDetailRow(
'Tanggal Diagnosa',
histori['tanggal_display'] ?? '',
Icons.calendar_today,
),
SizedBox(height: 16),
// Diagnosa
_buildDetailRow(
'Diagnosa',
histori['diagnosa'] ?? 'Tidak ada diagnosa',
Icons.medical_services,
),
SizedBox(height: 16),
// Hasil
_buildDetailRow(
'Hasil',
_formatHasil(histori['hasil']),
Icons.analytics,
),
SizedBox(height: 16),
// Gejala
_buildDetailSection(
'Gejala yang Dipilih',
gejalaText,
Icons.list_alt,
),
],
),
),
),
),
);
}
Widget _buildDetailRow(
String label,
String value,
IconData icon, {
Color? valueColor,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 20,
color: Color(0xFF9DC08D),
),
SizedBox(width: 12),
Expanded(
flex: 2,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.grey[700],
),
),
),
Text(
': ',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: valueColor ?? Colors.black87,
fontWeight: valueColor != null ? FontWeight.w500 : FontWeight.normal,
),
),
),
],
);
}
Widget _buildDetailSection(
String label,
String value,
IconData icon,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
size: 20,
color: Color(0xFF9DC08D),
),
SizedBox(width: 12),
Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.grey[700],
), ),
),
],
),
SizedBox(height: 8),
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
); );
} }

View File

@ -48,6 +48,25 @@ class _AdminPageState extends State<AdminPage> {
await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount); await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount);
} }
// Method untuk menghitung jumlah diagnosa berdasarkan tanggal unik
int _countUniqueByDate(List<dynamic> historiList) {
Set<String> uniqueDates = {};
for (var histori in historiList) {
// Ambil tanggal_diagnosa dari data
String? tanggalDiagnosa = histori['tanggal_diagnosa']?.toString();
if (tanggalDiagnosa != null && tanggalDiagnosa.isNotEmpty) {
// Extract hanya tanggal (YYYY-MM-DD) tanpa waktu
String dateOnly = tanggalDiagnosa.split(' ')[0];
uniqueDates.add(dateOnly);
}
}
print("Unique dates found: ${uniqueDates.toList()}");
return uniqueDates.length;
}
// Method untuk memuat data dashboard dari API // Method untuk memuat data dashboard dari API
Future<void> _loadDashboardData() async { Future<void> _loadDashboardData() async {
try { try {
@ -78,21 +97,33 @@ class _AdminPageState extends State<AdminPage> {
pestCount = hamaList.length; pestCount = hamaList.length;
print("Jumlah hama: $pestCount"); print("Jumlah hama: $pestCount");
// Modified diagnosis count logic print("Fetching histori data...");
// Mengambil data histori dan hitung berdasarkan tanggal unik
final allHistori = await ApiService().getAllHistori(); final allHistori = await ApiService().getAllHistori();
int currentCount = allHistori.length; print("Total histori records: ${allHistori.length}");
// Hitung jumlah diagnosa berdasarkan tanggal unik
int currentUniqueCount = _countUniqueByDate(allHistori);
print("Unique diagnosis dates: $currentUniqueCount");
if (currentCount > _lastKnownDiagnosisCount) { // Update diagnosis count berdasarkan tanggal unik
int newDiagnoses = currentCount - _lastKnownDiagnosisCount; if (currentUniqueCount > _lastKnownDiagnosisCount) {
int newDiagnoses = currentUniqueCount - _lastKnownDiagnosisCount;
diagnosisCount += newDiagnoses; diagnosisCount += newDiagnoses;
_lastKnownDiagnosisCount = currentCount; _lastKnownDiagnosisCount = currentUniqueCount;
// Save the updated counts // Save the updated counts
await _saveCounts(); await _saveCounts();
print("New diagnoses added: $newDiagnoses"); print("New unique diagnosis dates added: $newDiagnoses");
print("Total diagnosis count: $diagnosisCount"); print("Total diagnosis count: $diagnosisCount");
} else {
// Jika tidak ada penambahan, set ke current unique count
diagnosisCount = currentUniqueCount;
_lastKnownDiagnosisCount = currentUniqueCount;
await _saveCounts();
} }
} catch (e) { } catch (e) {
print("Error loading dashboard data: $e"); print("Error loading dashboard data: $e");
} finally { } finally {
@ -282,4 +313,4 @@ class _AdminPageState extends State<AdminPage> {
), ),
); );
} }
} }

View File

@ -288,24 +288,24 @@ class _EditHamaPageState extends State<EditHamaPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 20), SizedBox(height: 20),
TextField( // TextField(
controller: _nilaiPakarController, // controller: _nilaiPakarController,
decoration: InputDecoration( // decoration: InputDecoration(
labelText: 'Nilai Pakar', // labelText: 'Nilai Pakar',
hintText: 'Contoh: 0.5', // hintText: 'Contoh: 0.5',
), // ),
keyboardType: TextInputType.numberWithOptions(decimal: true), // keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) { // onChanged: (value) {
// Validate as user types (optional) // // Validate as user types (optional)
try { // try {
if (value.isNotEmpty) { // if (value.isNotEmpty) {
double.parse(value.replaceAll(',', '.')); // double.parse(value.replaceAll(',', '.'));
} // }
} catch (e) { // } catch (e) {
// Could show validation error here // // Could show validation error here
} // }
}, // },
), // ),
SizedBox(height: 20), SizedBox(height: 20),
Text( Text(
'Foto Hama', 'Foto Hama',

View File

@ -287,24 +287,24 @@ class _EditPenyakitPageState extends State<EditPenyakitPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 20), SizedBox(height: 20),
TextField( // TextField(
controller: _nilaiPakarController, // controller: _nilaiPakarController,
decoration: InputDecoration( // decoration: InputDecoration(
labelText: 'Nilai Pakar', // labelText: 'Nilai Pakar',
hintText: 'Contoh: 0.5', // hintText: 'Contoh: 0.5',
), // ),
keyboardType: TextInputType.numberWithOptions(decimal: true), // keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) { // onChanged: (value) {
// Validate as user types (optional) // // Validate as user types (optional)
try { // try {
if (value.isNotEmpty) { // if (value.isNotEmpty) {
double.parse(value.replaceAll(',', '.')); // double.parse(value.replaceAll(',', '.'));
} // }
} catch (e) { // } catch (e) {
// Could show validation error here // // Could show validation error here
} // }
}, // },
), // ),
SizedBox(height: 20), SizedBox(height: 20),
Text( Text(
'Foto Penyakit', 'Foto Penyakit',

View File

@ -9,13 +9,22 @@ class GejalaPage extends StatefulWidget {
class _GejalaPageState extends State<GejalaPage> { class _GejalaPageState extends State<GejalaPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> gejalaList = []; List<Map<String, dynamic>> gejalaList = [];
List<Map<String, dynamic>> filteredGejalaList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchGejala(); fetchGejala();
searchController.addListener(_filterGejala);
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
// 🔹 Ambil data gejala dari API // 🔹 Ambil data gejala dari API
Future<void> fetchGejala() async { Future<void> fetchGejala() async {
@ -23,12 +32,35 @@ class _GejalaPageState extends State<GejalaPage> {
final data = await apiService.getGejala(); final data = await apiService.getGejala();
setState(() { setState(() {
gejalaList = data; gejalaList = data;
filteredGejalaList = data;
}); });
} catch (e) { } catch (e) {
print('Error fetching gejala: $e'); print('Error fetching gejala: $e');
} }
} }
void _filterGejala() {
String query = searchController.text.toLowerCase();
setState(() {
filteredGejalaList = gejalaList.where((gejala) {
String nama = (gejala['nama'] ?? '').toLowerCase();
String kode = (gejala['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredGejalaList = gejalaList;
}
});
}
// 🔹 Tambah gejala baru ke API // 🔹 Tambah gejala baru ke API
void _tambahGejala() { void _tambahGejala() {
TextEditingController namaController = TextEditingController(); TextEditingController namaController = TextEditingController();
@ -70,51 +102,49 @@ class _GejalaPageState extends State<GejalaPage> {
} }
void showEditDialog(BuildContext context, Map<String, dynamic> gejala) { void showEditDialog(BuildContext context, Map<String, dynamic> gejala) {
final TextEditingController editNamaController = TextEditingController(text: gejala['nama'] ?? ''); final TextEditingController editNamaController = TextEditingController(text: gejala['nama'] ?? '');
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text( title: Text('Edit Gejala'),
'Edit Hama', content: Column(
), mainAxisSize: MainAxisSize.min,
content: Column( children: [
mainAxisSize: MainAxisSize.min, TextField(
children: [ controller: editNamaController,
TextField( decoration: InputDecoration(
controller: editNamaController, labelText: 'Nama',
decoration: InputDecoration( ),
labelText: 'Nama',
), ),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () async {
try {
await apiService.updateGejala(
gejala['id'],
editNamaController.text
);
fetchGejala();
Navigator.pop(context);
} catch (e) {
print("Error updating gejala: $e");
}
},
child: Text('Simpan', style: TextStyle(color: Colors.black)),
), ),
], ],
), );
actions: [ },
TextButton( );
onPressed: () => Navigator.pop(context), }
child: Text('Batal'),
),
ElevatedButton(
onPressed: () async {
try {
await apiService.updateGejala(
gejala['id'],
editNamaController.text
);
fetchGejala();
Navigator.pop(context);
} catch (e) {
print("Error updating gejala: $e");
}
},
child: Text('Simpan', style: TextStyle(color: Colors.black)),
),
],
);
},
);
}
// 🔹 Hapus gejala dari API // 🔹 Hapus gejala dari API
void _hapusGejala(int id) async { void _hapusGejala(int id) async {
@ -127,152 +157,194 @@ class _GejalaPageState extends State<GejalaPage> {
} }
void _konfirmasiHapus(int id) { void _konfirmasiHapus(int id) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), content: Text('Apakah Anda yakin ingin menghapus gejala ini?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); // Tutup pop-up tanpa menghapus Navigator.pop(context); // Tutup pop-up tanpa menghapus
}, },
child: Text('Tidak'), child: Text('Tidak'),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.pop(context); // Tutup pop-up Navigator.pop(context); // Tutup pop-up
_hapusGejala(id); // Lanjutkan proses hapus _hapusGejala(id); // Lanjutkan proses hapus
}, },
child: Text('Ya, Hapus'), child: Text('Ya, Hapus'),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
), ),
], ],
); );
}, },
); );
} }
//pagination
//pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = (start + rowsPerPage < gejalaList.length) int end = (start + rowsPerPage < filteredGejalaList.length)
? start + rowsPerPage ? start + rowsPerPage
: gejalaList.length; : filteredGejalaList.length;
List currentPageData = gejalaList.sublist(start, end); List currentPageData = filteredGejalaList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Halaman Gejala'), title: Text('Halaman Gejala'),
), backgroundColor: Color(0xFF9DC08D),
body: Column( ),
children: [ body: Column(
SizedBox(height: 20), children: [
Row( SizedBox(height: 16),
mainAxisAlignment: MainAxisAlignment.end, // Search Button
children: [ Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.only(right: 20.0), child: Row(
child: ElevatedButton( mainAxisAlignment: MainAxisAlignment.end,
onPressed: _tambahGejala, children: [
child: Text( ElevatedButton.icon(
'Tambah Gejala', onPressed: _toggleSearch,
style: TextStyle(color: Colors.green[200]), icon: Icon(Icons.search),
label: Text('Cari'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF9DC08D),
foregroundColor: Colors.white,
),
),
],
),
),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama atau kode gejala...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
), ),
), ),
), ),
], Expanded(
), child: Padding(
SizedBox(height: 20), padding: const EdgeInsets.all(16.0),
Expanded( child: Column(
child: Padding( children: [
padding: const EdgeInsets.all(8.0), // Data List
child: SingleChildScrollView( Expanded(
scrollDirection: Axis.vertical, child: ListView.builder(
child: SingleChildScrollView( itemCount: currentPageData.length,
scrollDirection: Axis.horizontal, itemBuilder: (context, index) {
child: DataTable( final gejala = currentPageData[index];
columnSpacing: 20,
headingRowColor: MaterialStateColor.resolveWith( return Container(
(states) => const Color(0xFF9DC08D), margin: EdgeInsets.only(bottom: 12),
), child: Row(
columns: [ children: [
DataColumn(label: SizedBox(width: 35, child: Text('No'))), // Card dengan nama gejala
DataColumn(label: SizedBox(width: 80, child: Text('Kode'))), Expanded(
DataColumn(label: SizedBox(width: 150, child: Text('Nama'))), child: Card(
DataColumn(label: SizedBox(width: 80, child: Text('Aksi'))), elevation: 2,
], child: InkWell(
rows: [ onTap: () => showEditDialog(context, gejala),
...currentPageData.map( child: Container(
(gejala) => DataRow( padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
cells: [ child: Column(
DataCell(Text((gejalaList.indexOf(gejala) + 1).toString())), crossAxisAlignment: CrossAxisAlignment.start,
DataCell(Text(gejala['kode'] ?? '-')), children: [
DataCell(Text(gejala['nama'] ?? '-')), Text(
DataCell( gejala['nama'] ?? '-',
Row( style: TextStyle(
children: [ fontSize: 16,
IconButton( fontWeight: FontWeight.w500,
icon: Icon(Icons.edit, color: Color(0xFF9DC08D)), ),
onPressed: () => showEditDialog(context, gejala), ),
SizedBox(height: 4),
Text(
gejala['kode'] ?? '-',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
),
), ),
IconButton( ),
icon: Icon(Icons.delete, color: Colors.red), 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: () => _konfirmasiHapus(gejala['id']), onPressed: () => _konfirmasiHapus(gejala['id']),
), ),
], ),
), ],
), ),
], );
), },
), ),
DataRow( ),
cells: [ SizedBox(height: 16),
DataCell(Container()), // Pagination
DataCell(Container()), if (filteredGejalaList.length > rowsPerPage)
DataCell( Row(
Align( mainAxisAlignment: MainAxisAlignment.center,
alignment: Alignment.centerRight, children: [
child: Row( IconButton(
mainAxisSize: MainAxisSize.min, icon: Icon(Icons.chevron_left),
children: [ onPressed: currentPage > 0
IconButton( ? () => setState(() => currentPage--)
icon: Icon(Icons.chevron_left), : null,
onPressed: currentPage > 0 ),
? () => setState(() => currentPage--) Text(
: null, 'Halaman ${currentPage + 1}',
), style: TextStyle(fontSize: 16),
Text(' ${currentPage + 1}'), ),
IconButton( IconButton(
icon: Icon(Icons.chevron_right), icon: Icon(Icons.chevron_right),
onPressed: onPressed: (currentPage + 1) * rowsPerPage < filteredGejalaList.length
(currentPage + 1) * rowsPerPage < gejalaList.length ? () => setState(() => currentPage++)
? () => setState(() => currentPage++) : null,
: null,
),
],
),
),
), ),
DataCell(Container()),
], ],
), ),
], ],
),
), ),
), ),
), ),
), ],
], ),
), floatingActionButton: FloatingActionButton(
); onPressed: _tambahGejala,
} child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
} ),
);
}
}

View File

@ -11,11 +11,21 @@ class HamaPage extends StatefulWidget {
class _HamaPageState extends State<HamaPage> { class _HamaPageState extends State<HamaPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> hamaList = []; List<Map<String, dynamic>> hamaList = [];
List<Map<String, dynamic>> filteredHamaList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchHama(); _fetchHama();
searchController.addListener(_filterHama);
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
Future<void> _fetchHama() async { Future<void> _fetchHama() async {
@ -23,12 +33,35 @@ class _HamaPageState extends State<HamaPage> {
List<Map<String, dynamic>> data = await apiService.getHama(); List<Map<String, dynamic>> data = await apiService.getHama();
setState(() { setState(() {
hamaList = data; hamaList = data;
filteredHamaList = data;
}); });
} catch (e) { } catch (e) {
print("Error fetching data: $e"); print("Error fetching data: $e");
} }
} }
void _filterHama() {
String query = searchController.text.toLowerCase();
setState(() {
filteredHamaList = hamaList.where((hama) {
String nama = (hama['nama'] ?? '').toLowerCase();
String kode = (hama['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredHamaList = hamaList;
}
});
}
// 🔹 Hapus gejala dari API // 🔹 Hapus gejala dari API
void _hapusHama(int id) async { void _hapusHama(int id) async {
try { try {
@ -45,7 +78,7 @@ class _HamaPageState extends State<HamaPage> {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), content: Text('Apakah Anda yakin ingin menghapus hama ini?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -67,6 +100,71 @@ class _HamaPageState extends State<HamaPage> {
); );
} }
void _navigateToEdit(Map<String, dynamic> hama) {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (hama['nilai_pakar'] != null) {
// Coba parse jika string
if (hama['nilai_pakar'] is String) {
try {
String nilaiStr = hama['nilai_pakar'].toString().trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
}
} catch (e) {
print("Error parsing nilai_pakar: $e");
}
}
// Langsung gunakan jika sudah double
else if (hama['nilai_pakar'] is double) {
nilaiPakar = hama['nilai_pakar'];
}
// Jika int, konversi ke double
else if (hama['nilai_pakar'] is int) {
nilaiPakar = hama['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditHamaPage(
idHama: hama['id'],
namaAwal: hama['nama'] ?? '',
deskripsiAwal: hama['deskripsi'] ?? '',
penangananAwal: hama['penanganan'] ?? '',
gambarUrl: hama['foto'] ?? '',
nilai_pakar: nilaiPakar,
onHamaUpdated: _fetchHama,
),
),
);
}
void _showPopupMenu(BuildContext context, int id) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Pilih Aksi'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.delete, color: Colors.red),
title: Text('Hapus Hama'),
onTap: () {
Navigator.pop(context);
_konfirmasiHapus(id);
},
),
],
),
);
},
);
}
//pagination //pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -74,203 +172,169 @@ class _HamaPageState extends State<HamaPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = int end = (start + rowsPerPage < filteredHamaList.length)
(start + rowsPerPage < hamaList.length) ? start + rowsPerPage
? start + rowsPerPage : filteredHamaList.length;
: hamaList.length; List currentPageData = filteredHamaList.sublist(start, end);
List currentPageData = hamaList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Halaman Hama'), backgroundColor: Color(0xFF9DC08D)), appBar: AppBar(
title: Text('Halaman Hama'),
backgroundColor: Color(0xFF9DC08D),
),
body: Column( body: Column(
children: [ children: [
SizedBox(height: 20), SizedBox(height: 16),
Row( // Search Button
mainAxisAlignment: MainAxisAlignment.end, Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
Padding( child: Row(
padding: const EdgeInsets.only(right: 20.0), mainAxisAlignment: MainAxisAlignment.end,
child: ElevatedButton( children: [
onPressed: () { ElevatedButton.icon(
Navigator.push( onPressed: _toggleSearch,
context, icon: Icon(Icons.search),
MaterialPageRoute( label: Text('Cari'),
builder: style: ElevatedButton.styleFrom(
(context) => TambahHamaPage( backgroundColor: Color(0xFF9DC08D),
onHamaAdded: foregroundColor: Colors.white,
_fetchHama, // Panggil fungsi refresh setelah tambah
),
),
);
},
child: Text(
'Tambah Hama',
style: TextStyle(color: Colors.green[200]),
), ),
), ),
],
),
),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama hama...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
),
), ),
], ),
),
SizedBox(height: 20),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: Column(
scrollDirection: Axis.vertical, children: [
child: SingleChildScrollView( // Data List
scrollDirection: Axis.horizontal, Expanded(
child: DataTable( child: ListView.builder(
columnSpacing: 20, itemCount: currentPageData.length,
headingRowColor: MaterialStateColor.resolveWith( itemBuilder: (context, index) {
(states) => const Color(0xFF9DC08D), final hama = currentPageData[index];
),
columns: [ return Container(
DataColumn(label: SizedBox(width: 35, child: Text('No'))), margin: EdgeInsets.only(bottom: 12),
DataColumn( child: Row(
label: SizedBox(width: 50, child: Text('Kode')), children: [
), // Card dengan nama hama
DataColumn( Expanded(
label: SizedBox(width: 100, child: Text('Nama')), child: Card(
), elevation: 2,
DataColumn( child: InkWell(
label: SizedBox(width: 100, child: Text('Deskripsi')), onTap: () => _navigateToEdit(hama),
), child: Container(
DataColumn( padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
label: SizedBox(width: 100, child: Text('Penanganan')), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
DataColumn( children: [
label: SizedBox(width: 50, child: Text('Aksi')), Text(
), hama['nama'] ?? '-',
], style: TextStyle(
rows: [ fontSize: 16,
...currentPageData.map( fontWeight: FontWeight.w500,
(hama) => DataRow( ),
cells: [ ),
DataCell( SizedBox(height: 4),
Text((hamaList.indexOf(hama) + 1).toString()), Text(
), hama['kode'] ?? '-',
DataCell(Text(hama['kode'] ?? '-')), style: TextStyle(
DataCell(Text(hama['nama'] ?? '-')), fontSize: 14,
DataCell(Text(hama['deskripsi'] ?? '-')), color: Colors.grey[600],
DataCell(Text(hama['penanganan'] ?? '-')), ),
DataCell( ),
Row( ],
children: [ ),
IconButton(
icon: Icon(
Icons.edit,
color: Color(0xFF9DC08D),
), ),
onPressed: () {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (hama['nilai_pakar'] != null) {
// Coba parse jika string
if (hama['nilai_pakar'] is String) {
try {
String nilaiStr =
hama['nilai_pakar']
.toString()
.trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(
nilaiStr.replaceAll(',', '.'),
);
}
} catch (e) {
print(
"Error parsing nilai_pakar: $e",
);
}
}
// Langsung gunakan jika sudah double
else if (hama['nilai_pakar']
is double) {
nilaiPakar = hama['nilai_pakar'];
}
// Jika int, konversi ke double
else if (hama['nilai_pakar'] is int) {
nilaiPakar =
hama['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => EditHamaPage(
idHama:
hama['id'], // pastikan 'hama' adalah Map dari API kamu
namaAwal: hama['nama'] ?? '',
deskripsiAwal:
hama['deskripsi'] ?? '',
penangananAwal:
hama['penanganan'] ?? '',
gambarUrl: hama['foto'] ?? '',
nilai_pakar: nilaiPakar,
onHamaUpdated:
_fetchHama, // fungsi untuk refresh list setelah update
),
),
);
},
), ),
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed:
() => _konfirmasiHapus(hama['id']),
),
],
), ),
), SizedBox(width: 8),
], // Button hapus di luar card
), Container(
), decoration: BoxDecoration(
DataRow( color: Colors.red,
cells: [ borderRadius: BorderRadius.circular(8),
DataCell(Container()), ),
DataCell(Container()), child: IconButton(
DataCell( icon: Icon(Icons.delete, color: Colors.white),
Align( onPressed: () => _konfirmasiHapus(hama['id']),
alignment: Alignment.centerRight, ),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
onPressed:
currentPage > 0
? () =>
setState(() => currentPage--)
: null,
),
Text(' ${currentPage + 1}'),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed:
(currentPage + 1) * rowsPerPage <
hamaList.length
? () =>
setState(() => currentPage++)
: null,
),
],
), ),
), ],
), ),
DataCell(Container()), );
DataCell(Container()), },
DataCell(Container()), ),
],
),
],
), ),
), SizedBox(height: 16),
// Pagination
if (filteredHamaList.length > rowsPerPage)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
onPressed: currentPage > 0
? () => setState(() => currentPage--)
: null,
),
Text(
'Halaman ${currentPage + 1}',
style: TextStyle(fontSize: 16),
),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed: (currentPage + 1) * rowsPerPage < filteredHamaList.length
? () => setState(() => currentPage++)
: null,
),
],
),
],
), ),
), ),
), ),
], ],
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TambahHamaPage(
onHamaAdded: _fetchHama,
),
),
);
},
child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
),
); );
} }
} }

View File

@ -12,11 +12,21 @@ class PenyakitPage extends StatefulWidget {
class _PenyakitPageState extends State<PenyakitPage> { class _PenyakitPageState extends State<PenyakitPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> penyakitList = []; List<Map<String, dynamic>> penyakitList = [];
List<Map<String, dynamic>> filteredPenyakitList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchPenyakit(); _fetchPenyakit();
searchController.addListener(_filterPenyakit);
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
Future<void> _fetchPenyakit() async { Future<void> _fetchPenyakit() async {
@ -24,19 +34,42 @@ class _PenyakitPageState extends State<PenyakitPage> {
List<Map<String, dynamic>> data = await apiService.getPenyakit(); List<Map<String, dynamic>> data = await apiService.getPenyakit();
setState(() { setState(() {
penyakitList = data; penyakitList = data;
filteredPenyakitList = data;
}); });
} catch (e) { } catch (e) {
print("Error fetching data: $e"); print("Error fetching data: $e");
} }
} }
// 🔹 Hapus gejala dari API void _filterPenyakit() {
String query = searchController.text.toLowerCase();
setState(() {
filteredPenyakitList = penyakitList.where((penyakit) {
String nama = (penyakit['nama'] ?? '').toLowerCase();
String kode = (penyakit['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredPenyakitList = penyakitList;
}
});
}
// 🔹 Hapus penyakit dari API
void _hapusPenyakit(int id) async { void _hapusPenyakit(int id) async {
try { try {
await apiService.deletePenyakit(id); await apiService.deletePenyakit(id);
_fetchPenyakit(); // Refresh data setelah hapus _fetchPenyakit(); // Refresh data setelah hapus
} catch (e) { } catch (e) {
print('Error hapus gejala: $e'); print('Error hapus penyakit: $e');
} }
} }
@ -46,7 +79,7 @@ class _PenyakitPageState extends State<PenyakitPage> {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), content: Text('Apakah Anda yakin ingin menghapus penyakit ini?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -68,6 +101,47 @@ class _PenyakitPageState extends State<PenyakitPage> {
); );
} }
void _navigateToEdit(Map<String, dynamic> penyakit) {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (penyakit['nilai_pakar'] != null) {
// Coba parse jika string
if (penyakit['nilai_pakar'] is String) {
try {
String nilaiStr = penyakit['nilai_pakar'].toString().trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
}
} catch (e) {
print("Error parsing nilai_pakar: $e");
}
}
// Langsung gunakan jika sudah double
else if (penyakit['nilai_pakar'] is double) {
nilaiPakar = penyakit['nilai_pakar'];
}
// Jika int, konversi ke double
else if (penyakit['nilai_pakar'] is int) {
nilaiPakar = penyakit['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditPenyakitPage(
idPenyakit: penyakit['id'],
namaAwal: penyakit['nama'] ?? '',
deskripsiAwal: penyakit['deskripsi'] ?? '',
penangananAwal: penyakit['penanganan'] ?? '',
gambarUrl: penyakit['foto'] ?? '',
nilai_pakar: nilaiPakar,
onPenyakitUpdated: _fetchPenyakit,
),
),
);
}
//pagination //pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -75,203 +149,169 @@ class _PenyakitPageState extends State<PenyakitPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = int end = (start + rowsPerPage < filteredPenyakitList.length)
(start + rowsPerPage < penyakitList.length) ? start + rowsPerPage
? start + rowsPerPage : filteredPenyakitList.length;
: penyakitList.length; List currentPageData = filteredPenyakitList.sublist(start, end);
List currentPageData = penyakitList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Halaman Penyakit'), backgroundColor: Color(0xFF9DC08D)), appBar: AppBar(
title: Text('Halaman Penyakit'),
backgroundColor: Color(0xFF9DC08D),
),
body: Column( body: Column(
children: [ children: [
SizedBox(height: 20), SizedBox(height: 16),
Row( // Search Button
mainAxisAlignment: MainAxisAlignment.end, Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
Padding( child: Row(
padding: const EdgeInsets.only(right: 20.0), mainAxisAlignment: MainAxisAlignment.end,
child: ElevatedButton( children: [
onPressed: () { ElevatedButton.icon(
Navigator.push( onPressed: _toggleSearch,
context, icon: Icon(Icons.search),
MaterialPageRoute( label: Text('Cari'),
builder: style: ElevatedButton.styleFrom(
(context) => TambahPenyakitPage( backgroundColor: Color(0xFF9DC08D),
onPenyakitAdded: foregroundColor: Colors.white,
_fetchPenyakit, // Panggil fungsi refresh setelah tambah
),
),
);
}, // Fungsi untuk menambah data penyakit
child: Text(
'Tambah Penyakit',
style: TextStyle(color: Colors.green[200]),
),
),
),
],
),
SizedBox(height: 20),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20,
headingRowColor: MaterialStateColor.resolveWith(
(states) => const Color(0xFF9DC08D),
),
columns: [
DataColumn(label: SizedBox(width: 35, child: Text('No'))),
DataColumn(
label: SizedBox(width: 50, child: Text('Kode')),
),
DataColumn(
label: SizedBox(width: 100, child: Text('Nama')),
),
DataColumn(
label: SizedBox(width: 100, child: Text('Deskripsi')),
),
DataColumn(
label: SizedBox(width: 100, child: Text('Penanganan')),
),
DataColumn(
label: SizedBox(width: 50, child: Text('Aksi')),
),
],
rows: [
...currentPageData.map(
(penyakit) => DataRow(
cells: [
DataCell(
Text((penyakitList.indexOf(penyakit) + 1).toString()),
),
DataCell(Text(penyakit['kode'] ?? '-')),
DataCell(Text(penyakit['nama'] ?? '-')),
DataCell(Text(penyakit['deskripsi'] ?? '-')),
DataCell(Text(penyakit['penanganan'] ?? '-')),
DataCell(
Row(
children: [
IconButton(
icon: Icon(
Icons.edit,
color: Color(0xFF9DC08D),
),
onPressed: () {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (penyakit['nilai_pakar'] != null) {
// Coba parse jika string
if (penyakit['nilai_pakar'] is String) {
try {
String nilaiStr =
penyakit['nilai_pakar']
.toString()
.trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(
nilaiStr.replaceAll(',', '.'),
);
}
} catch (e) {
print(
"Error parsing nilai_pakar: $e",
);
}
}
// Langsung gunakan jika sudah double
else if (penyakit['nilai_pakar']
is double) {
nilaiPakar = penyakit['nilai_pakar'];
}
// Jika int, konversi ke double
else if (penyakit['nilai_pakar'] is int) {
nilaiPakar =
penyakit['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => EditPenyakitPage(
idPenyakit:
penyakit['id'], // pastikan 'hama' adalah Map dari API kamu
namaAwal: penyakit['nama'] ?? '',
deskripsiAwal:
penyakit['deskripsi'] ?? '',
penangananAwal:
penyakit['penanganan'] ?? '',
gambarUrl:
penyakit['foto'] ?? '',
nilai_pakar: nilaiPakar,
onPenyakitUpdated:
_fetchPenyakit, // fungsi untuk refresh list setelah update
),
),
);
},
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed:
() => _konfirmasiHapus(penyakit['id']),
),
],
),
),
],
),
),
DataRow(
cells: [
DataCell(Container()),
DataCell(Container()),
DataCell(
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
onPressed:
currentPage > 0
? () =>
setState(() => currentPage--)
: null,
),
Text(' ${currentPage + 1}'),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed:
(currentPage + 1) * rowsPerPage <
penyakitList.length
? () =>
setState(() => currentPage++)
: null,
),
],
),
),
),
DataCell(Container()),
DataCell(Container()),
DataCell(Container()),
],
),
],
), ),
), ),
],
),
),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama atau kode penyakit...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Data List
Expanded(
child: ListView.builder(
itemCount: currentPageData.length,
itemBuilder: (context, index) {
final penyakit = currentPageData[index];
return Container(
margin: EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Card dengan nama penyakit
Expanded(
child: Card(
elevation: 2,
child: InkWell(
onTap: () => _navigateToEdit(penyakit),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
penyakit['nama'] ?? '-',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
penyakit['kode'] ?? '-',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
),
),
),
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: () => _konfirmasiHapus(penyakit['id']),
),
),
],
),
);
},
),
),
SizedBox(height: 16),
// Pagination
if (filteredPenyakitList.length > rowsPerPage)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
onPressed: currentPage > 0
? () => setState(() => currentPage--)
: null,
),
Text(
'Halaman ${currentPage + 1}',
style: TextStyle(fontSize: 16),
),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed: (currentPage + 1) * rowsPerPage < filteredPenyakitList.length
? () => setState(() => currentPage++)
: null,
),
],
),
],
), ),
), ),
), ),
], ],
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TambahPenyakitPage(
onPenyakitAdded: _fetchPenyakit,
),
),
);
},
child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
),
); );
} }
} }

View File

@ -3,7 +3,6 @@ import 'package:SIBAYAM/admin/edit_rule_page.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:SIBAYAM/api_services/api_services.dart'; import 'package:SIBAYAM/api_services/api_services.dart';
import 'tambah_rule_page.dart'; import 'tambah_rule_page.dart';
import 'edit_hama_page.dart';
class RulePage extends StatefulWidget { class RulePage extends StatefulWidget {
const RulePage({Key? key}) : super(key: key); const RulePage({Key? key}) : super(key: key);
@ -18,8 +17,13 @@ class _RulePageState extends State<RulePage> {
List<Map<String, dynamic>> hamaList = []; List<Map<String, dynamic>> hamaList = [];
List<dynamic> rules = []; List<dynamic> rules = [];
List<dynamic> filteredRules = [];
bool isLoading = true; bool isLoading = true;
// Search and filter variables
TextEditingController searchController = TextEditingController();
String selectedFilter = 'Semua'; // 'Semua', 'Penyakit', 'Hama'
// Pagination variables // Pagination variables
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -28,9 +32,57 @@ class _RulePageState extends State<RulePage> {
void initState() { void initState() {
super.initState(); super.initState();
fetchRules(); 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 { void fetchRules() async {
setState(() {
isLoading = true;
});
final apiService = ApiService(); final apiService = ApiService();
try { try {
@ -49,12 +101,12 @@ class _RulePageState extends State<RulePage> {
...rulesPenyakit.map((rule) { ...rulesPenyakit.map((rule) {
final gejala = gejalaList.firstWhere( final gejala = gejalaList.firstWhere(
(item) => item['id'] == rule['id_gejala'], (item) => item['id'] == rule['id_gejala'],
orElse: () => {'nama': '-'}, orElse: () => {'nama': 'Gejala tidak ditemukan'},
); );
final penyakit = penyakitList.firstWhere( final penyakit = penyakitList.firstWhere(
(item) => item['id'] == rule['id_penyakit'], (item) => item['id'] == rule['id_penyakit'],
orElse: () => {'nama': '-'}, orElse: () => {'nama': 'Penyakit tidak ditemukan'},
); );
return { return {
@ -66,20 +118,19 @@ class _RulePageState extends State<RulePage> {
'nama_penyakit': penyakit['nama'], 'nama_penyakit': penyakit['nama'],
'nama_hama': null, 'nama_hama': null,
'nilai_pakar': rule['nilai_pakar'], 'nilai_pakar': rule['nilai_pakar'],
'type': 'Penyakit',
}; };
}), }),
// Mengolah rules hama // Mengolah rules hama
...rulesHama.map((rule) { ...rulesHama.map((rule) {
// Mencari gejala berdasarkan id
final gejala = gejalaList.firstWhere( final gejala = gejalaList.firstWhere(
(item) => item['id'] == rule['id_gejala'], (item) => item['id'] == rule['id_gejala'],
orElse: () => {'nama': 'TIDAK DITEMUKAN'}, orElse: () => {'nama': 'Gejala tidak ditemukan'},
); );
// Mencari hama berdasarkan id
final hama = hamaList.firstWhere( final hama = hamaList.firstWhere(
(item) => item['id'] == rule['id_hama'], (item) => item['id'] == rule['id_hama'],
orElse: () => {'nama': 'TIDAK DITEMUKAN'}, orElse: () => {'nama': 'Hama tidak ditemukan'},
); );
return { return {
@ -91,15 +142,23 @@ class _RulePageState extends State<RulePage> {
'nama_penyakit': null, 'nama_penyakit': null,
'nama_hama': hama['nama'], 'nama_hama': hama['nama'],
'nilai_pakar': rule['nilai_pakar'], 'nilai_pakar': rule['nilai_pakar'],
'type': 'Hama',
}; };
}), }),
]; ];
setState(() { setState(() {
rules = enrichedRules; rules = enrichedRules;
filteredRules = enrichedRules;
}); });
} catch (e) { } catch (e) {
print('Terjadi kesalahan saat memuat data: $e'); print('Terjadi kesalahan saat memuat data: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal memuat data: $e'),
backgroundColor: Colors.red,
),
);
} finally { } finally {
setState(() { setState(() {
isLoading = false; isLoading = false;
@ -108,21 +167,47 @@ class _RulePageState extends State<RulePage> {
} }
Future<void> deleteRule(Map<String, dynamic> rule) async { 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 { try {
http.Response res; http.Response res;
// Tentukan fungsi delete berdasarkan isi rule // Tentukan fungsi delete berdasarkan isi rule
if (rule['id_hama'] != null) { if (rule['id_hama'] != null) {
res = await ApiService.deleteRuleHama(rule['id']); // Fungsi API untuk delete hama res = await ApiService.deleteRuleHama(rule['id']);
} else if (rule['id_penyakit'] != null) { } else if (rule['id_penyakit'] != null) {
res = await ApiService.deleteRulePenyakit(rule['id']); // Fungsi API untuk delete penyakit res = await ApiService.deleteRulePenyakit(rule['id']);
} else { } else {
throw Exception("Data rule tidak valid (tidak ada id_hama atau id_penyakit)"); throw Exception("Data rule tidak valid");
} }
if (res.statusCode == 200) { if (res.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Rule berhasil dihapus")) SnackBar(
content: Text("Rule berhasil dihapus"),
backgroundColor: Colors.green,
),
); );
fetchRules(); // Refresh data setelah delete fetchRules(); // Refresh data setelah delete
} else { } else {
@ -130,7 +215,10 @@ class _RulePageState extends State<RulePage> {
} }
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan saat menghapus: $e")), SnackBar(
content: Text("Terjadi kesalahan saat menghapus: $e"),
backgroundColor: Colors.red,
),
); );
} }
} }
@ -138,224 +226,420 @@ class _RulePageState extends State<RulePage> {
// Get paginated data // Get paginated data
List<dynamic> get paginatedRules { List<dynamic> get paginatedRules {
final startIndex = currentPage * rowsPerPage; final startIndex = currentPage * rowsPerPage;
final endIndex = startIndex + rowsPerPage > rules.length ? rules.length : startIndex + rowsPerPage; final endIndex = startIndex + rowsPerPage > filteredRules.length
? filteredRules.length
: startIndex + rowsPerPage;
if (startIndex >= rules.length) { if (startIndex >= filteredRules.length) {
return []; return [];
} }
return rules.sublist(startIndex, endIndex); return filteredRules.sublist(startIndex, endIndex);
} }
@override Widget _buildSearchAndFilter() {
Widget build(BuildContext context) { return Card(
return Scaffold( elevation: 2,
appBar: AppBar(title: const Text('Data Rules'), backgroundColor: Color(0xFF9DC08D)), child: Padding(
body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
// Button untuk tambah rule hama Expanded(
ElevatedButton.icon( flex: 2,
onPressed: () { child: TextField(
Navigator.push( controller: searchController,
context, decoration: InputDecoration(
MaterialPageRoute( hintText: 'Cari berdasarkan nama penyakit, hama, atau gejala...',
builder: (context) => TambahRulePage( prefixIcon: Icon(Icons.search),
isEditing: false, border: OutlineInputBorder(
isEditingHama: true, // Menandakan ini adalah rule hama borderRadius: BorderRadius.circular(8),
selectedRuleIds: [],
selectedGejalaIds: [],
nilaiPakarList: [],
selectedHamaId: null,
selectedPenyakitId: null,
showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown hama
),
), ),
).then((_) => fetchRules()); contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
}, ),
icon: Icon(Icons.bug_report, size: 16,),
label: Text(
"Tambah Rule Hama",
style: TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
), ),
), ),
SizedBox(width: 10), SizedBox(width: 16),
// Button untuk tambah rule penyakit Expanded(
ElevatedButton.icon( child: DropdownButtonFormField<String>(
onPressed: () { value: selectedFilter,
Navigator.push( decoration: InputDecoration(
context, labelText: 'Filter Kategori',
MaterialPageRoute( border: OutlineInputBorder(
builder: (context) => TambahRulePage( borderRadius: BorderRadius.circular(8),
isEditing: false,
isEditingHama: false, // Menandakan ini adalah rule penyakit
selectedRuleIds: [],
selectedGejalaIds: [],
nilaiPakarList: [],
selectedHamaId: null,
selectedPenyakitId: null,
showPenyakitOnly: true, // Parameter baru untuk menampilkan hanya dropdown penyakit
),
), ),
).then((_) => fetchRules()); contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
}, ),
icon: Icon(Icons.healing, size: 16,), items: ['Semua', 'Penyakit', 'Hama'].map((String value) {
label: Text( return DropdownMenuItem<String>(
"Tambah Rule Penyakit", value: value,
style: TextStyle(fontSize: 12),), child: Text(value),
style: ElevatedButton.styleFrom( );
backgroundColor: Colors.blue, }).toList(),
foregroundColor: Colors.white, onChanged: (String? newValue) {
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil setState(() {
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil selectedFilter = newValue!;
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap _filterRules();
});
},
), ),
), ),
], ],
), ),
const SizedBox(height: 16), SizedBox(height: 16),
isLoading Row(
? const Center(child: CircularProgressIndicator()) mainAxisAlignment: MainAxisAlignment.spaceBetween,
: Expanded( children: [
child: Column( Text(
children: [ 'Total: ${filteredRules.length} rule(s)',
Expanded( style: TextStyle(
child: SingleChildScrollView( fontWeight: FontWeight.w500,
scrollDirection: Axis.horizontal, color: Colors.grey[600],
child: SingleChildScrollView( ),
scrollDirection: Axis.vertical, ),
child: DataTable( Row(
headingRowColor: MaterialStateProperty.resolveWith<Color>( children: [
(Set<MaterialState> states) { ElevatedButton.icon(
return Color(0xFF9DC08D); // Apply color to all header rows onPressed: () {
}, Navigator.push(
), context,
columns: const [ MaterialPageRoute(
DataColumn(label: Text('No')), builder: (context) => TambahRulePage(
DataColumn(label: Text('Hama & Penyakit')), isEditing: false,
DataColumn(label: Text('Gejala')), isEditingHama: true,
DataColumn(label: Text('Nilai Pakar')), selectedRuleIds: [],
DataColumn(label: Text('Aksi')), selectedGejalaIds: [],
], nilaiPakarList: [],
rows: List.generate(paginatedRules.length, (index) { selectedHamaId: null,
final rule = paginatedRules[index]; selectedPenyakitId: null,
final displayIndex = currentPage * rowsPerPage + index + 1; showHamaOnly: true,
final namaKategori = rule['id_penyakit'] != null
? rule['nama_penyakit'] ?? '-'
: rule['nama_hama'] ?? '-';
final isPenyakit = rule['id_penyakit'] != null;
return DataRow(
cells: [
DataCell(Text(displayIndex.toString())),
DataCell(Text(namaKategori)),
DataCell(Text(rule['nama_gejala'] ?? '-')),
DataCell(Text(rule['nilai_pakar']?.toString() ?? '-')),
DataCell(
Row(
children: [
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.orange,
),
onPressed: () {
if (rule != null &&
rule['id'] != null &&
rule['id_gejala'] != null &&
rule['nilai_pakar'] != null) {
// Tentukan jenis rule untuk editing
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?,
// Tambahkan parameter untuk menentukan dropdown yang ditampilkan
showHamaOnly: editingHama,
showPenyakitOnly: !editingHama,
),
),
).then((_) => fetchRules());
} else {
// Tampilkan pesan error jika data rule tidak lengkap
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Data rule tidak lengkap atau tidak valid"),
backgroundColor: Colors.red,
),
);
// Debug info
print("Rule data: $rule");
}
},
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () {
deleteRule(rule);
},
),
],
),
),
],
);
}),
), ),
), ),
), ).then((_) => fetchRules());
), },
// Pagination controls icon: Icon(Icons.bug_report, size: 16),
Container( label: Text("Rule Hama", style: TextStyle(fontSize: 12)),
padding: EdgeInsets.symmetric(vertical: 8.0), style: ElevatedButton.styleFrom(
child: Row( backgroundColor: Colors.green,
mainAxisAlignment: MainAxisAlignment.center, foregroundColor: Colors.white,
children: [ padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
IconButton( minimumSize: Size(0, 36),
icon: Icon(Icons.chevron_left), ),
onPressed: currentPage > 0
? () => setState(() => currentPage--)
: null,
),
Text('Halaman ${currentPage + 1} dari ${(rules.length / rowsPerPage).ceil()}'),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed: (currentPage + 1) * rowsPerPage < rules.length
? () => setState(() => currentPage++)
: null,
),
],
),
),
],
), ),
), 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(),
],
),
),
],
),
),
);
}
} }

View File

@ -38,36 +38,43 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
} }
Future<void> _simpanHama() async { Future<void> _simpanHama() async {
if (namaController.text.isNotEmpty && if (namaController.text.isNotEmpty &&
deskripsiController.text.isNotEmpty && deskripsiController.text.isNotEmpty &&
penangananController.text.isNotEmpty) { penangananController.text.isNotEmpty) {
try { try {
double? nilaipakar; // Kirim nilai pakar sebagai double, 0.0 jika tidak diisi
if (nilaiPakarController.text.isNotEmpty) { double nilaiPakar = 0.0;
String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); if (nilaiPakarController.text.trim().isNotEmpty) {
nilaipakar = double.parse(nilaiInput); String nilaiInput = nilaiPakarController.text.replaceAll(',', '.');
nilaiPakar = double.parse(nilaiInput);
}
print("Debug - Nilai Pakar Double: $nilaiPakar");
print("Debug - Nama: ${namaController.text}");
print("Debug - Deskripsi: ${deskripsiController.text}");
print("Debug - Penanganan: ${penangananController.text}");
print("Debug - Nilai Pakar: $nilaiPakar");
print("Debug - Image File: $_pickedFile");
await apiService.createHama(
namaController.text,
deskripsiController.text,
penangananController.text,
_pickedFile,
nilaiPakar,
);
widget.onHamaAdded();
Navigator.pop(context);
_showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
} catch (e) {
_showDialog('Gagal', 'Gagal menambahkan data hama: $e');
print("Error adding hama: $e");
} }
} else {
await apiService.createHama( _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.');
namaController.text,
deskripsiController.text,
penangananController.text,
_pickedFile,
nilaipakar, // boleh null
);
widget.onHamaAdded();
Navigator.pop(context);
_showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
} catch (e) {
_showDialog('Gagal', 'Gagal menambahkan data hama.');
print("Error adding hama: $e");
} }
} else {
_showDialog('Error', 'Semua field harus diisi (kecuali nilai pakar).');
} }
}
void _showDialog(String title, String message) { void _showDialog(String title, String message) {
showDialog( showDialog(
@ -149,8 +156,11 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
SizedBox(height: 15), SizedBox(height: 15),
// TextField( // TextField(
// controller: nilaiPakarController, // controller: nilaiPakarController,
// decoration: InputDecoration(labelText: 'Nilai Pakar'), // decoration: InputDecoration(
// maxLines: 3, // labelText: 'Nilai Pakar (Optional)',
// hintText: 'Masukkan nilai pakar (opsional)',
// ),
// keyboardType: TextInputType.numberWithOptions(decimal: true),
// ), // ),
SizedBox(height: 15), SizedBox(height: 15),
Text('Foto'), Text('Foto'),
@ -197,4 +207,4 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
), ),
); );
} }
} }

View File

@ -39,27 +39,39 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
Future<void> _simpanPenyakit() async { Future<void> _simpanPenyakit() async {
if (namaController.text.isNotEmpty && if (namaController.text.isNotEmpty &&
deskripsiController.text.isNotEmpty && deskripsiController.text.isNotEmpty &&
penangananController.text.isNotEmpty && penangananController.text.isNotEmpty) {
nilaiPakarController.text.isNotEmpty) {
try { try {
String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); // Kirim nilai pakar sebagai double, 0.0 jika tidak diisi
double nilaiPakar = double.parse(nilaiInput); double nilaiPakar = 0.0;
if (nilaiPakarController.text.trim().isNotEmpty) {
String nilaiInput = nilaiPakarController.text.replaceAll(',', '.');
nilaiPakar = double.parse(nilaiInput);
}
print("Debug - Nilai Pakar Double: $nilaiPakar");
print("Debug - Nama: ${namaController.text}");
print("Debug - Deskripsi: ${deskripsiController.text}");
print("Debug - Penanganan: ${penangananController.text}");
print("Debug - Nilai Pakar: $nilaiPakar");
print("Debug - Image File: $_pickedFile");
await apiService.createPenyakit( await apiService.createPenyakit(
namaController.text, namaController.text,
deskripsiController.text, deskripsiController.text,
penangananController.text, penangananController.text,
_pickedFile, _pickedFile,
nilaiPakar, nilaiPakar,
); );
widget.onPenyakitAdded(); widget.onPenyakitAdded();
Navigator.pop(context); Navigator.pop(context);
_showDialog('Berhasil', 'Data penyakit berhasil ditambahkan.'); _showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
} catch (e) { } catch (e) {
_showDialog('Gagal', 'Gagal menambahkan data penyakit.'); _showDialog('Gagal', 'Gagal menambahkan data hama: $e');
print("Error adding penyakit: $e"); print("Error adding hama: $e");
} }
} else { } else {
_showDialog('Error', 'Semua field harus diisi.'); _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.');
} }
} }
@ -141,11 +153,11 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 15), SizedBox(height: 15),
TextField( // TextField(
controller: nilaiPakarController, // controller: nilaiPakarController,
decoration: InputDecoration(labelText: 'nilai pakar'), // decoration: InputDecoration(labelText: 'nilai pakar'),
maxLines: 3, // maxLines: 3,
), // ),
SizedBox(height: 15), SizedBox(height: 15),
(_webImage != null) (_webImage != null)
? Image.memory( ? Image.memory(

File diff suppressed because it is too large Load Diff

View File

@ -899,7 +899,7 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
String deskripsi, String deskripsi,
String penanganan, String penanganan,
XFile? pickedFile, XFile? pickedFile,
double nilai_pakar double? nilai_pakar
) async { ) async {
try { try {
var uri = Uri.parse(penyakitUrl); var uri = Uri.parse(penyakitUrl);

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:SIBAYAM/user/before_login.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -62,7 +63,9 @@ class _LoginPageState extends State<LoginPage> {
} }
try { try {
var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login"); var url = Uri.parse(
"https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login",
);
var response = await http.post( var response = await http.post(
url, url,
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@ -120,7 +123,10 @@ class _LoginPageState extends State<LoginPage> {
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white), icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BeforeLogin()),
);
}, },
), ),
), ),