diff --git a/backend/controller/diagnosaController.js b/backend/controller/diagnosaController.js new file mode 100644 index 0000000..e190b2b --- /dev/null +++ b/backend/controller/diagnosaController.js @@ -0,0 +1,102 @@ +const { Rule_penyakit, Rule_hama, Gejala, Penyakit, Hama } = require('../models'); + +exports.diagnosa = async (req, res) => { + const { gejala } = req.body; // array of id_gejala + + if (!gejala || !Array.isArray(gejala)) { + return res.status(400).json({ message: 'Gejala harus berupa array' }); + } + + try { + // ===================== Penyakit ===================== + const allPenyakitRules = await Rule_penyakit.findAll({ + where: { + id_gejala: gejala, + }, + include: [ + { + model: Penyakit, + as: 'penyakit', + }, + ], + }); + + const penyakitScores = {}; + + allPenyakitRules.forEach(rule => { + const idPenyakit = rule.id_penyakit; + const nilaiPakarGejala = rule.nilai_pakar; // P(E|H) + const nilaiPakarPenyakit = rule.penyakit.nilai_pakar; // P(H) + + if (!penyakitScores[idPenyakit]) { + // === Menginisialisasi: P(E|H) * P(H) === + penyakitScores[idPenyakit] = { + penyakit: rule.penyakit.nama, + total: nilaiPakarGejala * nilaiPakarPenyakit, // ← Rumus Bayes awal + }; + } else { + // === Mengalikan P(E|H) berikutnya (jika diasumsikan independen) === + penyakitScores[idPenyakit].total *= nilaiPakarGejala; + } + }); + + // ===================== Hama ===================== + const allHamaRules = await Rule_hama.findAll({ + where: { + id_gejala: gejala, + }, + include: [ + { + model: Hama, + as: 'hama', + }, + ], + }); + + const hamaScores = {}; + + allHamaRules.forEach(rule => { + const idHama = rule.id_hama; + const nilaiPakarGejala = rule.nilai_pakar; // P(E|H) + const nilaiPakarHama = rule.hama.nilai_pakar; // P(H) + + if (!hamaScores[idHama]) { + // === Menginisialisasi: P(E|H) * P(H) === + hamaScores[idHama] = { + hama: rule.hama.nama, + total: nilaiPakarGejala * nilaiPakarHama, // ← Rumus Bayes awal + }; + } else { + // === Mengalikan P(E|H) berikutnya === + hamaScores[idHama].total *= nilaiPakarGejala; + } + }); + + // ===================== Normalisasi (opsional) ===================== + const totalPenyakit = Object.values(penyakitScores).reduce((acc, cur) => acc + cur.total, 0); + const totalHama = Object.values(hamaScores).reduce((acc, cur) => acc + cur.total, 0); + + const normalizedPenyakit = Object.values(penyakitScores).map(p => ({ + ...p, + probabilitas: (p.total / totalPenyakit) || 0, // Probabilitas akhir + })); + + const normalizedHama = Object.values(hamaScores).map(h => ({ + ...h, + probabilitas: (h.total / totalHama) || 0, + })); + + // Sorting + const sortedPenyakit = normalizedPenyakit.sort((a, b) => b.probabilitas - a.probabilitas); + const sortedHama = normalizedHama.sort((a, b) => b.probabilitas - a.probabilitas); + + res.json({ + penyakit: sortedPenyakit, + hama: sortedHama, + }); + + } catch (error) { + console.error('Error dalam perhitungan Bayes:', error); + res.status(500).json({ message: 'Terjadi kesalahan dalam proses diagnosa' }); + } +}; diff --git a/backend/controller/gejalaController.js b/backend/controller/gejalaController.js index 5f18990..1a39be0 100644 --- a/backend/controller/gejalaController.js +++ b/backend/controller/gejalaController.js @@ -4,7 +4,7 @@ const {Gejala} = require('../models'); exports.getAllGejala = async (req, res) => { try { const gejala = await Gejala.findAll({ - attributes: ['id', 'nama'] + attributes: ['id', 'nama', 'kode'] }); res.status(200).json(gejala); } catch (error) { diff --git a/backend/controller/hamaController.js b/backend/controller/hamaController.js index 14e3308..28f04fc 100644 --- a/backend/controller/hamaController.js +++ b/backend/controller/hamaController.js @@ -6,7 +6,7 @@ const fs = require('fs'); exports.getAllHama = async (req, res) => { try { const dataHama = await Hama.findAll({ - attributes: ['id', 'nama' , 'deskripsi' , 'penanganan', 'foto'] + attributes: ['id', 'nama' , 'deskripsi' , 'penanganan', 'foto', 'kode', 'nilai_pakar'] }); res.status(200).json({ message: 'Data hama berhasil diambil', data: dataHama }); } catch (error) { @@ -51,7 +51,7 @@ exports.getHamaById = async (req, res) => { // Pastikan sudah import 'Hama' model dan multer middleware sebelumnya exports.createHama = async (req, res) => { try { - const { nama, deskripsi, penanganan } = req.body; + const { nama, deskripsi, penanganan, nilai_pakar } = req.body; const file = req.file; // Cek kode terakhir @@ -71,10 +71,11 @@ exports.createHama = async (req, res) => { const newHama = await Hama.create({ kode: newKode, nama, - kategori: 'hama', // Default kategori + kategori: 'hama', deskripsi, penanganan, - foto: fotoPath, // ⬅️ Masukkan nama file ke database + foto: fotoPath, + nilai_pakar }); res.status(201).json({ message: 'Hama berhasil ditambahkan', data: newHama }); @@ -88,7 +89,7 @@ exports.createHama = async (req, res) => { exports.updateHama = async (req, res) => { try { const { id } = req.params; - const { nama, kategori, deskripsi, penanganan } = req.body; + const { nama, kategori, deskripsi, penanganan, nilai_pakar, } = req.body; const hama = await Hama.findByPk(id); if (!hama) { @@ -101,7 +102,7 @@ exports.updateHama = async (req, res) => { foto = req.file.filename; } - await hama.update({ nama, kategori, deskripsi, penanganan, foto }); + await hama.update({ nama, kategori, deskripsi, penanganan, foto, nilai_pakar }); res.status(200).json({ message: 'Hama berhasil diperbarui', data: hama }); } catch (error) { diff --git a/backend/controller/penyakitController.js b/backend/controller/penyakitController.js index 390ecec..1ebd6a3 100644 --- a/backend/controller/penyakitController.js +++ b/backend/controller/penyakitController.js @@ -6,7 +6,7 @@ const fs = require('fs'); exports.getAllPenyakit = async (req, res) => { try { const dataPenyakit = await Penyakit.findAll({ - attributes: ['id', 'nama' , 'deskripsi' , 'penanganan' , 'foto'] + attributes: ['id', 'nama' , 'deskripsi' , 'penanganan' , 'foto', 'kode', 'nilai_pakar'] }); res.status(200).json({ message: 'Data penyakit berhasil diambil', data: dataPenyakit }); } catch (error) { @@ -50,7 +50,7 @@ exports.getPenyakitById = async (req, res) => { // 🔹 Fungsi untuk menambahkan penyakit baru (kode otomatis & kategori default) exports.createPenyakit = async (req, res) => { try { - const { nama, deskripsi, penanganan } = req.body; + const { nama, deskripsi, penanganan, nilai_pakar } = req.body; const file = req.file; // Cek kode terakhir @@ -74,6 +74,7 @@ exports.createPenyakit = async (req, res) => { deskripsi, penanganan, foto: fotoPath, + nilai_pakar }); res.status(201).json({ message: 'Penyakit berhasil ditambahkan', data: newPenyakit }); @@ -86,7 +87,7 @@ exports.createPenyakit = async (req, res) => { exports.updatePenyakit = async (req, res) => { try { const { id } = req.params; - const { nama, kategori, deskripsi, penanganan } = req.body; + const { nama, kategori, deskripsi, penanganan, nilai_pakar } = req.body; const penyakit = await Penyakit.findByPk(id); if (!penyakit) { @@ -99,7 +100,7 @@ exports.updatePenyakit = async (req, res) => { foto = req.file.filename; } - await penyakit.update({ nama, kategori, deskripsi, penanganan, foto }); + await penyakit.update({ nama, kategori, deskripsi, penanganan, foto, nilai_pakar }); res.status(200).json({ message: 'Penyakit berhasil diperbarui', data: penyakit }); } catch (error) { diff --git a/backend/image_hama/hama-1746382661292.jpg b/backend/image_hama/hama-1746382661292.jpg new file mode 100644 index 0000000..fa920a5 Binary files /dev/null and b/backend/image_hama/hama-1746382661292.jpg differ diff --git a/backend/image_penyakit/penyakit-1746385635429.jpg b/backend/image_penyakit/penyakit-1746385635429.jpg new file mode 100644 index 0000000..80c7ff0 Binary files /dev/null and b/backend/image_penyakit/penyakit-1746385635429.jpg differ diff --git a/backend/migrations/20250318213037-create-hama.js b/backend/migrations/20250318213037-create-hama.js index 2e89447..33e4c40 100644 --- a/backend/migrations/20250318213037-create-hama.js +++ b/backend/migrations/20250318213037-create-hama.js @@ -1,4 +1,7 @@ 'use strict'; + +const { sequelize } = require('../models'); + /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { @@ -24,6 +27,10 @@ module.exports = { type: Sequelize.STRING, allowNull: true }, + nilai_pakar: { + type: Sequelize.FLOAT, + allowNull: true + }, }); }, async down(queryInterface, Sequelize) { diff --git a/backend/migrations/20250504181953-add-nilai_pakar-to-penyakit.js b/backend/migrations/20250504181953-add-nilai_pakar-to-penyakit.js new file mode 100644 index 0000000..f042ce9 --- /dev/null +++ b/backend/migrations/20250504181953-add-nilai_pakar-to-penyakit.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('penyakit', 'nilai_pakar', { + type: Sequelize.FLOAT, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('penyakit', 'nilai_pakar'); + } +}; diff --git a/backend/models/hama.js b/backend/models/hama.js index de4727c..b647d70 100644 --- a/backend/models/hama.js +++ b/backend/models/hama.js @@ -31,6 +31,10 @@ module.exports = (sequelize) => { foto: { type: DataTypes.STRING, allowNull: false, + }, + nilai_pakar: { + type: DataTypes.FLOAT, + allowNull: true } }, { diff --git a/backend/models/penyakit.js b/backend/models/penyakit.js index 92c9bbf..abc6e57 100644 --- a/backend/models/penyakit.js +++ b/backend/models/penyakit.js @@ -31,6 +31,10 @@ module.exports =(sequelize) => { foto: { type: DataTypes.STRING, allowNull: false, + }, + nilai_pakar: { + type: DataTypes.FLOAT, + allowNull: true } }, { diff --git a/backend/routes/diagnosaRoutes.js b/backend/routes/diagnosaRoutes.js new file mode 100644 index 0000000..d0cee5b --- /dev/null +++ b/backend/routes/diagnosaRoutes.js @@ -0,0 +1,42 @@ +const express = require('express'); +const router = express.Router(); +const diagnosaController = require('../controller/diagnosaController'); + +/** + * @swagger + * /api/diagnosa/bayes: + * post: + * summary: Melakukan diagnosa penyakit dan hama menggunakan Teorema Bayes + * tags: [Diagnosa] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * gejala: + * type: array + * items: + * type: integer + * example: [1, 2, 3] + * responses: + * 200: + * description: Hasil diagnosa berhasil dikembalikan + * content: + * application/json: + * schema: + * type: object + * properties: + * penyakit: + * type: object + * hama: + * type: object + * 400: + * description: Permintaan tidak valid + * 500: + * description: Terjadi kesalahan pada server + */ +router.post('/bayes', diagnosaController.diagnosaBayes); + +module.exports = router; diff --git a/backend/routes/hamaRoutes.js b/backend/routes/hamaRoutes.js index e09e941..bf834c6 100644 --- a/backend/routes/hamaRoutes.js +++ b/backend/routes/hamaRoutes.js @@ -68,6 +68,9 @@ router.get('/:id/image', hamaController.getHamaById); * type: string * format: binary * description: Foto hama (JPG, JPEG, PNG, GIF) + * nilai_pakar: + * type: number + * format: float * responses: * 201: * description: Hama berhasil ditambahkan @@ -105,6 +108,9 @@ router.post('/', uploadHamaGambar.single('foto'), hamaController.createHama); * foto: * type: string * format: binary + * nilai_pakar: + * type: number + * format: float * responses: * 200: * description: Hama berhasil diperbarui diff --git a/backend/routes/penyakitRoutes.js b/backend/routes/penyakitRoutes.js index 3859dd7..645cd3d 100644 --- a/backend/routes/penyakitRoutes.js +++ b/backend/routes/penyakitRoutes.js @@ -69,6 +69,9 @@ router.get('/:id/image', penyakitController.getPenyakitById); * type: string * format: binary * description: Foto penyakit (JPG, JPEG, PNG, GIF) + * nilai_pakar: + * type: number + * format: float * responses: * 201: * description: Hama berhasil ditambahkan @@ -106,6 +109,9 @@ router.post('/', uploadPenyakitGambar.single('foto'), penyakitController.createP * foto: * type: string * format: binary + * nilai_pakar: + * type: number + * format: float * responses: * 200: * description: penyakit berhasil diperbarui diff --git a/frontend/lib/admin/admin_page.dart b/frontend/lib/admin/admin_page.dart index 69206ea..e8b8028 100644 --- a/frontend/lib/admin/admin_page.dart +++ b/frontend/lib/admin/admin_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'hama_page.dart'; import 'penyakit_page.dart'; import 'gejala_page.dart'; @@ -6,7 +7,65 @@ import 'rule_page.dart'; import 'package:frontend/api_services/api_services.dart'; import 'package:frontend/user/login_page.dart'; -class AdminPage extends StatelessWidget { +class AdminPage extends StatefulWidget { + @override + _AdminPageState createState() => _AdminPageState(); +} + +class _AdminPageState extends State { + // Data counters + int userCount = 0; + int diagnosisCount = 0; + int diseaseCount = 0; + int pestCount = 0; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadDashboardData(); + } + + // Method untuk memuat data dashboard dari API + Future _loadDashboardData() async { + try { + setState(() { + isLoading = true; + }); + + print("Fetching users with role 'user'..."); + + // Mengambil jumlah user dengan role 'user' + final userList = await ApiService().getUsers(role: 'user'); + if (userList != null && userList.isNotEmpty) { + userCount = userList.length; + print("Jumlah user: $userCount"); + } else { + print("Tidak ada user dengan role 'user'."); + } + + print("Fetching data penyakit..."); + // Mengambil data penyakit menggunakan fungsi yang sudah ada + final penyakitList = await ApiService().getPenyakit(); + diseaseCount = penyakitList.length; + print("Jumlah penyakit: $diseaseCount"); + + print("Fetching data hama..."); + // Mengambil data hama menggunakan fungsi yang sudah ada + final hamaList = await ApiService().getHama(); + pestCount = hamaList.length; + print("Jumlah hama: $pestCount"); + + } catch (e) { + print("Error loading dashboard data: $e"); + } finally { + setState(() { + isLoading = false; + }); + } +} + + Future _logout(BuildContext context) async { await ApiService.logoutUser(); Navigator.pushReplacement( @@ -18,7 +77,10 @@ class AdminPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Admin Dashboard')), + appBar: AppBar( + title: Text('Admin Dashboard'), + backgroundColor: Color(0xFF9DC08D), + ), drawer: Drawer( child: Container( color: Color(0xFFFFFFFF), @@ -80,41 +142,61 @@ class AdminPage extends StatelessWidget { body: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Selamat datang Admin!', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - SizedBox(height: 24), - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildCard('Jumlah User', '10'), - _buildCard('Jumlah Diagnosa', '25'), - ], - ), - SizedBox(height: 16), // Spasi antar baris - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildCard('Penyakit', '15'), - _buildCard('Hama', '15'), - ], - ), - ], - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat datang Admin!', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + SizedBox(height: 24), + isLoading + ? Center( + child: CircularProgressIndicator(color: Color(0xFF9DC08D)), + ) + : Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCard( + 'Jumlah User', + userCount.toString(), + Icons.people, + ), + _buildCard( + 'Jumlah Diagnosa', + diagnosisCount.toString(), + Icons.assignment, + ), + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCard( + 'Penyakit', + diseaseCount.toString(), + Icons.sick, + ), + _buildCard( + 'Hama', + pestCount.toString(), + Icons.bug_report, + ), + ], + ), + ], + ), + ], + ), ), ), ); } - Widget _buildCard(String title, String count) { + Widget _buildCard(String title, String count, IconData icon) { return Card( elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -125,13 +207,22 @@ class AdminPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(icon, size: 40, color: Color(0xFF9DC08D)), + SizedBox(height: 10), Text( title, textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 10), - Text(count, style: TextStyle(fontSize: 20, color: Colors.green)), + Text( + count, + style: TextStyle( + fontSize: 24, + color: Color(0xFF9DC08D), + fontWeight: FontWeight.bold, + ), + ), ], ), ), diff --git a/frontend/lib/admin/edit_hama_page.dart b/frontend/lib/admin/edit_hama_page.dart index 9391e23..c9476bb 100644 --- a/frontend/lib/admin/edit_hama_page.dart +++ b/frontend/lib/admin/edit_hama_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:frontend/api_services/api_services.dart'; -import 'image_utilities.dart'; // Import file baru +import 'image_utilities.dart'; import 'dart:io'; import 'dart:typed_data'; import 'package:image_picker/image_picker.dart'; @@ -11,6 +11,7 @@ class EditHamaPage extends StatefulWidget { final String namaAwal; final String deskripsiAwal; final String penangananAwal; + final double nilai_pakar; final String gambarUrl; final VoidCallback onHamaUpdated; @@ -20,6 +21,7 @@ class EditHamaPage extends StatefulWidget { required this.namaAwal, required this.deskripsiAwal, required this.penangananAwal, + required this.nilai_pakar, required this.gambarUrl, required this.onHamaUpdated, }) : super(key: key); @@ -32,6 +34,7 @@ class _EditHamaPageState extends State { final TextEditingController _namaController = TextEditingController(); final TextEditingController _deskripsiController = TextEditingController(); final TextEditingController _penangananController = TextEditingController(); + final TextEditingController _nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); @@ -41,6 +44,8 @@ class _EditHamaPageState extends State { String? _errorMessage; bool _isImageLoading = false; Uint8List? _currentImageBytes; + // Default value for nilai_pakar to prevent empty string issues + double _currentNilaiPakar = 0.0; @override void initState() { @@ -49,6 +54,10 @@ class _EditHamaPageState extends State { _deskripsiController.text = widget.deskripsiAwal; _penangananController.text = widget.penangananAwal; + // Ensure nilai_pakar is properly initialized + _currentNilaiPakar = widget.nilai_pakar; + _nilaiPakarController.text = widget.nilai_pakar.toString(); + // Load existing image _loadExistingImage(); } @@ -89,9 +98,25 @@ class _EditHamaPageState extends State { _namaController.dispose(); _deskripsiController.dispose(); _penangananController.dispose(); + _nilaiPakarController.dispose(); super.dispose(); } + // Validate and parse nilai_pakar input + double _parseNilaiPakar() { + if (_nilaiPakarController.text.isEmpty) { + return _currentNilaiPakar; // Return current value if field is empty + } + + try { + String input = _nilaiPakarController.text.trim().replaceAll(',', '.'); + return double.parse(input); + } catch (e) { + print("Error parsing nilai_pakar: $e"); + return _currentNilaiPakar; // Return current value if parsing fails + } + } + Future _updateHama() async { try { setState(() { @@ -99,12 +124,18 @@ class _EditHamaPageState extends State { _errorMessage = null; }); + // Get nilai_pakar value with safety check + double nilaiPakar = _parseNilaiPakar(); + + print("Updating hama with nilai_pakar: $nilaiPakar"); + await apiService.updateHama( widget.idHama, _namaController.text, _deskripsiController.text, _penangananController.text, _pickedFile, + nilaiPakar, ); setState(() { @@ -257,41 +288,71 @@ class _EditHamaPageState extends State { maxLines: 3, ), SizedBox(height: 20), - Text( - 'Foto Hama', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - SizedBox(height: 8), - _buildImagePreview(), - SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: _pickImage, - icon: Icon(Icons.photo_library), - label: Text('Pilih Gambar'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ), - ], - ), - - ElevatedButton( - onPressed: _updateHama, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[300], + TextField( + controller: _nilaiPakarController, + decoration: InputDecoration( + labelText: 'Nilai Pakar', + hintText: 'Contoh: 0.5', ), - child: Text( - 'Simpan Perubahan', - style: TextStyle(color: Colors.black), + keyboardType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + // Validate as user types (optional) + try { + if (value.isNotEmpty) { + double.parse(value.replaceAll(',', '.')); + } + } catch (e) { + // Could show validation error here + } + }, + ), + SizedBox(height: 20), + Text( + 'Foto Hama', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), ), + SizedBox(height: 8), + _buildImagePreview(), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: _pickImage, + icon: Icon(Icons.photo_library), + label: Text('Pilih Gambar'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ], + ), + SizedBox(height: 20), + if (_isLoading) + CircularProgressIndicator() + else + ElevatedButton( + onPressed: _updateHama, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[300], + ), + child: Text( + 'Simpan Perubahan', + style: TextStyle(color: Colors.black), + ), + ), + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.red), + ), + ), ], ), ), @@ -301,4 +362,4 @@ class _EditHamaPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/edit_penyakit_page.dart b/frontend/lib/admin/edit_penyakit_page.dart index f605fdf..00647c5 100644 --- a/frontend/lib/admin/edit_penyakit_page.dart +++ b/frontend/lib/admin/edit_penyakit_page.dart @@ -12,6 +12,7 @@ class EditPenyakitPage extends StatefulWidget { final String namaAwal; final String deskripsiAwal; final String penangananAwal; + final double nilai_pakar; final String gambarUrl; final VoidCallback onPenyakitUpdated; @@ -21,6 +22,7 @@ class EditPenyakitPage extends StatefulWidget { required this.namaAwal, required this.deskripsiAwal, required this.penangananAwal, + required this.nilai_pakar, required this.gambarUrl, required this.onPenyakitUpdated, }) : super(key: key); @@ -33,6 +35,7 @@ class _EditPenyakitPageState extends State { final TextEditingController _namaController = TextEditingController(); final TextEditingController _deskripsiController = TextEditingController(); final TextEditingController _penangananController = TextEditingController(); + final TextEditingController _nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); @@ -42,6 +45,7 @@ class _EditPenyakitPageState extends State { String? _errorMessage; bool _isImageLoading = false; Uint8List? _currentImageBytes; + double _currentNilaiPakar = 0.0; @override void initState() { @@ -49,6 +53,8 @@ class _EditPenyakitPageState extends State { _namaController.text = widget.namaAwal; _deskripsiController.text = widget.deskripsiAwal; _penangananController.text = widget.penangananAwal; + _currentNilaiPakar = widget.nilai_pakar; + _nilaiPakarController.text = widget.nilai_pakar.toString(); _loadExistingImage(); } @@ -91,6 +97,21 @@ class _EditPenyakitPageState extends State { super.dispose(); } + // Validate and parse nilai_pakar input + double _parseNilaiPakar() { + if (_nilaiPakarController.text.isEmpty) { + return _currentNilaiPakar; // Return current value if field is empty + } + + try { + String input = _nilaiPakarController.text.trim().replaceAll(',', '.'); + return double.parse(input); + } catch (e) { + print("Error parsing nilai_pakar: $e"); + return _currentNilaiPakar; // Return current value if parsing fails + } + } + Future _updatePenyakit() async { try { setState(() { @@ -98,12 +119,18 @@ class _EditPenyakitPageState extends State { _errorMessage = null; }); + // Get nilai_pakar value with safety check + double nilaiPakar = _parseNilaiPakar(); + + print("Updating hama with nilai_pakar: $nilaiPakar"); + await apiService.updatePenyakit( widget.idPenyakit, _namaController.text, _deskripsiController.text, _penangananController.text, _pickedFile, + nilaiPakar, ); setState(() { @@ -260,6 +287,25 @@ class _EditPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 20), + TextField( + controller: _nilaiPakarController, + decoration: InputDecoration( + labelText: 'Nilai Pakar', + hintText: 'Contoh: 0.5', + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + // Validate as user types (optional) + try { + if (value.isNotEmpty) { + double.parse(value.replaceAll(',', '.')); + } + } catch (e) { + // Could show validation error here + } + }, + ), + SizedBox(height: 20), Text( 'Foto Penyakit', style: TextStyle( diff --git a/frontend/lib/admin/edit_rule_page.dart b/frontend/lib/admin/edit_rule_page.dart index 7678974..77c8e08 100644 --- a/frontend/lib/admin/edit_rule_page.dart +++ b/frontend/lib/admin/edit_rule_page.dart @@ -13,6 +13,8 @@ class EditRulePage extends StatefulWidget { final List nilaiPakarList; final int? selectedHamaId; final int? selectedPenyakitId; + final bool showHamaOnly; // Tambahkan ini + final bool showPenyakitOnly; // Tambahkan ini const EditRulePage({ Key? key, @@ -21,6 +23,8 @@ class EditRulePage extends StatefulWidget { required this.selectedRuleIds, required this.selectedGejalaIds, required this.nilaiPakarList, + this.showHamaOnly = false, // Tambahkan default value + this.showPenyakitOnly = false, this.selectedHamaId, this.selectedPenyakitId, }) : super(key: key); @@ -37,6 +41,9 @@ class _EditRulePageState extends State { bool isLoading = true; + bool showHamaOnly = false; + bool showPenyakitOnly = false; + final api = ApiService(); // Deklarasi variabel untuk menampung data dari API @@ -154,6 +161,8 @@ class _EditRulePageState extends State { @override void initState() { super.initState(); + showHamaOnly = widget.showHamaOnly; + showPenyakitOnly = widget.showPenyakitOnly; fetchData(); // Panggil fetchData saat halaman dibuka pertama kali // Inisialisasi dari widget parent @@ -250,62 +259,68 @@ class _EditRulePageState extends State { child: ListView( children: [ // Pilih Hama - Text("Pilih Hama"), - DropdownButton( - isExpanded: true, - value: selectedHamaId, - hint: Text('Pilih Hama'), - items: - hamaList.isNotEmpty - ? hamaList.map>((hama) { - return DropdownMenuItem( - value: hama['id'], - child: Text(hama['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedHamaId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showPenyakitOnly) ...[ + Text("Pilih Hama"), + DropdownButton( + isExpanded: true, + value: selectedHamaId, + hint: Text('Pilih Hama'), + items: + hamaList.isNotEmpty + ? hamaList.map>(( + hama, + ) { + return DropdownMenuItem( + value: hama['id'], + child: Text(hama['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedHamaId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Penyakit - Text("Pilih Penyakit"), - DropdownButton( - isExpanded: true, - value: selectedPenyakitId, - hint: Text('Pilih Penyakit'), - items: - penyakitList.isNotEmpty - ? penyakitList.map>(( - penyakit, - ) { - return DropdownMenuItem( - value: penyakit['id'], - child: Text(penyakit['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedPenyakitId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showHamaOnly) ...[ + Text("Pilih Penyakit"), + DropdownButton( + isExpanded: true, + value: selectedPenyakitId, + hint: Text('Pilih Penyakit'), + items: + penyakitList.isNotEmpty + ? penyakitList.map>(( + penyakit, + ) { + return DropdownMenuItem( + value: penyakit['id'], + child: Text(penyakit['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedPenyakitId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Gejala dan Nilai Pakar Text("Pilih Gejala"), diff --git a/frontend/lib/admin/hama_page.dart b/frontend/lib/admin/hama_page.dart index 8b5dec9..7abee0a 100644 --- a/frontend/lib/admin/hama_page.dart +++ b/frontend/lib/admin/hama_page.dart @@ -67,128 +67,6 @@ class _HamaPageState extends State { ); } - // void _tambahHama() { - // TextEditingController namaController = TextEditingController(); - // TextEditingController penangananController = TextEditingController(); - // TextEditingController deskripsiController = TextEditingController(); - - // showDialog( - // context: context, - // builder: (context) { - // return AlertDialog( - // title: Text('Tambah Hama Baru'), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // TextField( - // controller: namaController, - // decoration: InputDecoration(labelText: 'Nama'), - // ), - // TextField( - // controller: deskripsiController, - // decoration: InputDecoration(labelText: 'Deskripsi'), - // ), - // TextField( - // controller: penangananController, - // decoration: InputDecoration(labelText: 'Penanganan'), - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: () => Navigator.pop(context), - // child: Text('Batal', style: TextStyle(color: Colors.black)), - // ), - // ElevatedButton( - // onPressed: () async { - // if (namaController.text.isNotEmpty && - // deskripsiController.text.isNotEmpty && - // penangananController.text.isNotEmpty) { - // try { - // await apiService.createHama( - // namaController.text, - // deskripsiController.text, - // penangananController.text, - // ); - // _fetchHama(); - // Navigator.pop(context); - // } catch (e) { - // print("Error adding hama: $e"); - // } - // } - // }, - // child: Text('Simpan', style: TextStyle(color: Colors.black)), - // ), - // ], - // ); - // }, - // ).then((_) { - // namaController.dispose(); - // deskripsiController.dispose(); - // penangananController.dispose(); - // }); - // } - - // void showEditDialog(BuildContext context, Map hama) { - // final TextEditingController editNamaController = TextEditingController( - // text: hama['nama'] ?? '', - // ); - // final TextEditingController editDeskripsiController = TextEditingController( - // text: hama['deskripsi'] ?? '', - // ); - // final TextEditingController editPenangananController = - // TextEditingController(text: hama['penanganan'] ?? ''); - - // showDialog( - // context: context, - // builder: (context) { - // return AlertDialog( - // title: Text('Edit Hama'), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // TextField( - // controller: editNamaController, - // decoration: InputDecoration(labelText: 'Nama'), - // ), - // TextField( - // controller: editDeskripsiController, - // decoration: InputDecoration(labelText: 'Deskripsi'), - // ), - // TextField( - // controller: editPenangananController, - // decoration: InputDecoration(labelText: 'Penanganan'), - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: () => Navigator.pop(context), - // child: Text('Batal'), - // ), - // ElevatedButton( - // onPressed: () async { - // try { - // await apiService.updateHama( - // hama['id'], - // editNamaController.text, - // editDeskripsiController.text, - // editPenangananController.text, - // ); - // _fetchHama(); - // Navigator.pop(context); - // } catch (e) { - // print("Error updating hama: $e"); - // } - // }, - // child: Text('Simpan', style: TextStyle(color: Colors.black)), - // ), - // ], - // ); - // }, - // ); - // } - //pagination int currentPage = 0; int rowsPerPage = 10; @@ -283,6 +161,38 @@ class _HamaPageState extends State { 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( @@ -296,6 +206,7 @@ class _HamaPageState extends State { penangananAwal: hama['penanganan'] ?? '', gambarUrl: hama['foto'] ?? '', + nilai_pakar: nilaiPakar, onHamaUpdated: _fetchHama, // fungsi untuk refresh list setelah update ), diff --git a/frontend/lib/admin/penyakit_page.dart b/frontend/lib/admin/penyakit_page.dart index 66923b3..337e03b 100644 --- a/frontend/lib/admin/penyakit_page.dart +++ b/frontend/lib/admin/penyakit_page.dart @@ -162,6 +162,38 @@ class _PenyakitPageState extends State { 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( @@ -176,6 +208,7 @@ class _PenyakitPageState extends State { penyakit['penanganan'] ?? '', gambarUrl: penyakit['foto'] ?? '', + nilai_pakar: nilaiPakar, onPenyakitUpdated: _fetchPenyakit, // fungsi untuk refresh list setelah update ), diff --git a/frontend/lib/admin/rule_page.dart b/frontend/lib/admin/rule_page.dart index 2f8f7bf..d5e88b2 100644 --- a/frontend/lib/admin/rule_page.dart +++ b/frontend/lib/admin/rule_page.dart @@ -176,141 +176,176 @@ class _RulePageState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => TambahRulePage( - isEditing: false, // Menandakan mode tambah - isEditingHama: - true, // Atur sesuai dengan jenis rule - selectedRuleIds: [], - selectedGejalaIds: [], - nilaiPakarList: [], - selectedHamaId: null, // Hanya jika rule hama - selectedPenyakitId: - null, // Hanya jika rule penyakit - ), - ), - ).then((_) => fetchRules()); - }, - icon: Icon(Icons.add), - label: Text("Tambah Rule"), - ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Button untuk tambah rule hama + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + isEditing: false, + isEditingHama: true, // Menandakan ini adalah rule hama + selectedRuleIds: [], + selectedGejalaIds: [], + nilaiPakarList: [], + selectedHamaId: null, + selectedPenyakitId: null, + showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown hama + ), + ), + ).then((_) => fetchRules()); + }, + icon: Icon(Icons.bug_report), + label: Text("Tambah Rule Hama"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + SizedBox(width: 10), + // Button untuk tambah rule penyakit + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + 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()); + }, + icon: Icon(Icons.healing), + label: Text("Tambah Rule Penyakit"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ], ), const SizedBox(height: 16), isLoading ? const Center(child: CircularProgressIndicator()) : Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columns: const [ - DataColumn(label: Text('No')), - DataColumn(label: Text('Hama / Penyakit')), - DataColumn(label: Text('Gejala')), - DataColumn(label: Text('nilai pakar')), - DataColumn(label: Text('Aksi')), - ], - rows: List.generate(rules.length, (index) { - final rule = rules[index]; + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columns: const [ + DataColumn(label: Text('No')), + DataColumn(label: Text('Hama / Penyakit')), + DataColumn(label: Text('Gejala')), + DataColumn(label: Text('nilai pakar')), + DataColumn(label: Text('Aksi')), + ], + rows: List.generate(rules.length, (index) { + final rule = rules[index]; - final namaKategori = - rule['id_penyakit'] != null - ? rule['nama_penyakit'] ?? '-' - : rule['nama_hama'] ?? '-'; + final namaKategori = + rule['id_penyakit'] != null + ? rule['nama_penyakit'] ?? '-' + : rule['nama_hama'] ?? '-'; - final isPenyakit = rule['id_penyakit'] != null; + final isPenyakit = rule['id_penyakit'] != null; - return DataRow( - cells: [ - DataCell(Text((index + 1).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) { - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => EditRulePage( - isEditing: true, - isEditingHama: true, - 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 type cast ke int? - ), - ), - ); - } 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); - }, - ), - ], + return DataRow( + cells: [ + DataCell(Text((index + 1).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); + }, + ), + ], + ), + ), + ], + ); + }), + ), ), ), - ), ], ), ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/tambah_hama_page.dart b/frontend/lib/admin/tambah_hama_page.dart index 7566ad7..8d9605e 100644 --- a/frontend/lib/admin/tambah_hama_page.dart +++ b/frontend/lib/admin/tambah_hama_page.dart @@ -19,6 +19,7 @@ class _TambahHamaPageState extends State { final TextEditingController namaController = TextEditingController(); final TextEditingController deskripsiController = TextEditingController(); final TextEditingController penangananController = TextEditingController(); + final TextEditingController nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); File? _imageFile; @@ -32,19 +33,24 @@ class _TambahHamaPageState extends State { namaController.dispose(); deskripsiController.dispose(); penangananController.dispose(); + nilaiPakarController.dispose(); super.dispose(); } Future _simpanHama() async { if (namaController.text.isNotEmpty && deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty) { + penangananController.text.isNotEmpty && + nilaiPakarController.text.isNotEmpty) { try { + String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); + double nilaiPakar = double.parse(nilaiInput); await apiService.createHama( namaController.text, deskripsiController.text, penangananController.text, - _pickedFile, + _pickedFile, + nilaiPakar, ); widget.onHamaAdded(); Navigator.pop(context); @@ -136,6 +142,12 @@ class _TambahHamaPageState extends State { maxLines: 3, ), SizedBox(height: 15), + TextField( + controller: nilaiPakarController, + decoration: InputDecoration(labelText: 'Nilai Pakar'), + maxLines: 3, + ), + SizedBox(height: 15), Text('Foto'), (_webImage != null) ? Image.memory( diff --git a/frontend/lib/admin/tambah_penyakit_page.dart b/frontend/lib/admin/tambah_penyakit_page.dart index d908fe4..51f6daa 100644 --- a/frontend/lib/admin/tambah_penyakit_page.dart +++ b/frontend/lib/admin/tambah_penyakit_page.dart @@ -18,6 +18,7 @@ class _TambahPenyakitPageState extends State { final TextEditingController namaController = TextEditingController(); final TextEditingController deskripsiController = TextEditingController(); final TextEditingController penangananController = TextEditingController(); + final TextEditingController nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); File? _imageFile; @@ -31,19 +32,24 @@ class _TambahPenyakitPageState extends State { namaController.dispose(); deskripsiController.dispose(); penangananController.dispose(); + nilaiPakarController.dispose(); super.dispose(); } Future _simpanPenyakit() async { if (namaController.text.isNotEmpty && deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty) { + penangananController.text.isNotEmpty && + nilaiPakarController.text.isNotEmpty) { try { + String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); + double nilaiPakar = double.parse(nilaiInput); await apiService.createPenyakit( namaController.text, deskripsiController.text, penangananController.text, _pickedFile, + nilaiPakar, ); widget.onPenyakitAdded(); Navigator.pop(context); @@ -135,6 +141,12 @@ class _TambahPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 15), + TextField( + controller: nilaiPakarController, + decoration: InputDecoration(labelText: 'nilai pakar'), + maxLines: 3, + ), + SizedBox(height: 15), (_webImage != null) ? Image.memory( _webImage!, diff --git a/frontend/lib/admin/tambah_rule_page.dart b/frontend/lib/admin/tambah_rule_page.dart index 539538b..64e6327 100644 --- a/frontend/lib/admin/tambah_rule_page.dart +++ b/frontend/lib/admin/tambah_rule_page.dart @@ -13,6 +13,8 @@ class TambahRulePage extends StatefulWidget { final List nilaiPakarList; final int? selectedHamaId; final int? selectedPenyakitId; + final bool showHamaOnly; // Tambahkan ini + final bool showPenyakitOnly; const TambahRulePage({ Key? key, @@ -23,6 +25,8 @@ class TambahRulePage extends StatefulWidget { required this.nilaiPakarList, this.selectedHamaId, this.selectedPenyakitId, + this.showHamaOnly = false, // Tambahkan default value + this.showPenyakitOnly = false, }) : super(key: key); } @@ -31,12 +35,16 @@ class _TambahRulePageState extends State { int? selectedPenyakitId; List selectedGejalaIds = [null]; List nilaiPakarList = [0.5]; - List selectedRuleIds = []; // List paralel dengan selectedGejalaIds dan nilaiPakarList + List selectedRuleIds = + []; // List paralel dengan selectedGejalaIds dan nilaiPakarList bool isEditing = true; // atau false jika sedang edit penyakit bool isLoading = true; + bool showHamaOnly = false; + bool showPenyakitOnly = false; + final api = ApiService(); // Deklarasi variabel untuk menampung data dari API @@ -119,6 +127,8 @@ class _TambahRulePageState extends State { @override void initState() { super.initState(); + showHamaOnly = widget.showHamaOnly; + showPenyakitOnly = widget.showPenyakitOnly; fetchData(); // Panggil fetchData saat halaman dibuka pertama kali } @@ -173,57 +183,6 @@ class _TambahRulePageState extends State { } } - // void updateRules() async { - // if (selectedPenyakitId == null && selectedHamaId == null) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text("Pilih minimal satu: Penyakit atau Hama")), - // ); - // return; - // } - - // try { - // for (int i = 0; i < selectedGejalaIds.length; i++) { - // final idRule = selectedRuleIds[i]; - // final idGejala = selectedGejalaIds[i]; - // final nilai = nilaiPakarList[i]; - - // if (idRule != null && idGejala != null) { - // http.Response response; - - // if (selectedPenyakitId != null) { - // response = await ApiService.updateRulePenyakit( - // id: idRule, - // idGejala: idGejala, - // idPenyakit: selectedPenyakitId, - // nilaiPakar: nilai, - // ); - // } else { - // response = await ApiService.updateRuleHama( - // id: idRule, - // idGejala: idGejala, - // idHama: selectedHamaId, - // nilaiPakar: nilai, - // ); - // } - - // if (response.statusCode != 200 && response.statusCode != 201) { - // throw Exception("Gagal mengupdate rule"); - // } - // } - // } - - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(SnackBar(content: Text("Data berhasil diperbarui"))); - // Navigator.pop(context); - // } catch (e) { - // print('Gagal memperbarui data: $e'); - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(SnackBar(content: Text("Gagal memperbarui data"))); - // } - // } - @override Widget build(BuildContext context) { return Scaffold( @@ -243,62 +202,68 @@ class _TambahRulePageState extends State { child: ListView( children: [ // Pilih Hama - Text("Pilih Hama"), - DropdownButton( - isExpanded: true, - value: selectedHamaId, - hint: Text('Pilih Hama'), - items: - hamaList.isNotEmpty - ? hamaList.map>((hama) { - return DropdownMenuItem( - value: hama['id'], - child: Text(hama['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedHamaId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showPenyakitOnly) ...[ + Text("Pilih Hama"), + DropdownButton( + isExpanded: true, + value: selectedHamaId, + hint: Text('Pilih Hama'), + items: + hamaList.isNotEmpty + ? hamaList.map>(( + hama, + ) { + return DropdownMenuItem( + value: hama['id'], + child: Text(hama['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedHamaId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Penyakit - Text("Pilih Penyakit"), - DropdownButton( - isExpanded: true, - value: selectedPenyakitId, - hint: Text('Pilih Penyakit'), - items: - penyakitList.isNotEmpty - ? penyakitList.map>(( - penyakit, - ) { - return DropdownMenuItem( - value: penyakit['id'], - child: Text(penyakit['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedPenyakitId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showHamaOnly) ...[ + Text("Pilih Penyakit"), + DropdownButton( + isExpanded: true, + value: selectedPenyakitId, + hint: Text('Pilih Penyakit'), + items: + penyakitList.isNotEmpty + ? penyakitList.map>(( + penyakit, + ) { + return DropdownMenuItem( + value: penyakit['id'], + child: Text(penyakit['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedPenyakitId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Gejala dan Nilai Pakar Text("Pilih Gejala"), @@ -398,7 +363,33 @@ class _TambahRulePageState extends State { // Tombol untuk menambah rule ElevatedButton( onPressed: () { - // Panggil fungsi saveRules untuk menyimpan data + // Cek duplikasi gejala + final uniqueGejala = selectedGejalaIds.toSet(); + if (uniqueGejala.length != + selectedGejalaIds.length) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Terdapat gejala yang sama, harap pilih gejala yang berbeda.', + ), + ), + ); + return; // Gagal simpan + } + + // Cek apakah semua nilai gejala sudah dipilih (tidak null) + if (selectedGejalaIds.contains(null)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Harap lengkapi semua pilihan gejala.', + ), + ), + ); + return; + } + + // Panggil fungsi saveRules jika valid saveRules(); }, child: Text('Tambah Rule'), diff --git a/frontend/lib/api_services/api_services.dart b/frontend/lib/api_services/api_services.dart index 477b4bb..e9ef531 100644 --- a/frontend/lib/api_services/api_services.dart +++ b/frontend/lib/api_services/api_services.dart @@ -13,6 +13,7 @@ class ApiService { static const String penyakitUrl = 'http://localhost:5000/api/penyakit'; static const String rulesPenyakitUrl ='http://localhost:5000/api/rules_penyakit'; static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama'; + static const String userUrl = 'http://localhost:5000/api/users'; static const Duration timeout = Duration(seconds: 15); // Fungsi Login (dengan perbaikan) @@ -123,7 +124,6 @@ class ApiService { } // Ambil semua hama - // Get all hama Future>> getHama() async { try { final response = await http.get(Uri.parse(hamaUrl)).timeout(timeout); @@ -251,6 +251,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse(hamaUrl); @@ -259,6 +260,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); print('Mengirim request ke: $uri'); print('Dengan fields: ${request.fields}'); @@ -330,6 +332,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse('$hamaUrl/$id'); @@ -339,6 +342,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); // Log untuk debugging print('Mengirim request ke: $uri'); @@ -524,12 +528,33 @@ class ApiService { } } + Future getPenyakitImageBytesByFilename(String filename) async { + try { + final url = Uri.parse('http://localhost:5000/image_penyakit/$filename'); + print('Fetching image from: $url'); + final response = await http.get(url); + + if (response.statusCode == 200) { + return response.bodyBytes; + } else { + print('Failed to fetch image. Status: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error fetching image by filename: $e'); + return null; + } +} + + // Tambah penyakit baru (kode otomatis) Future> createPenyakit( String nama, String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse(penyakitUrl); @@ -538,6 +563,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); print('Mengirim request ke: $uri'); print('Dengan fields: ${request.fields}'); @@ -609,6 +635,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse('$penyakitUrl/$id'); @@ -618,6 +645,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); // Log untuk debugging print('Mengirim request ke: $uri'); @@ -889,4 +917,39 @@ class ApiService { final response = await http.delete(Uri.parse('$rulesHamaUrl/$id')); return response; } + + //get users + Future>> getUsers({String? role}) async { + try { + String url = ApiService.userUrl; + if (role != null) { + url += '?role=$role'; + } + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final List responseData = jsonDecode(response.body); + + // Filter berdasarkan role jika perlu + if (role != null) { + // Mengambil user dengan role yang sesuai + final filteredData = responseData.where((user) => user['role'] == role).toList(); + return List>.from(filteredData); + } + + // Jika tidak ada filter role, kembalikan semua data + return List>.from(responseData); + } else { + throw Exception("Gagal mengambil data user: ${response.statusCode}"); + } + } catch (e) { + print("Error getUsers: $e"); + throw Exception("Gagal mengambil data user"); + } } + + +} + + diff --git a/frontend/lib/user/detail_hama_page.dart b/frontend/lib/user/detail_hama_page.dart index 3b6690e..2ff5f8c 100644 --- a/frontend/lib/user/detail_hama_page.dart +++ b/frontend/lib/user/detail_hama_page.dart @@ -57,23 +57,40 @@ class _DetailHamaPageState extends State { // Widget untuk menampilkan gambar dengan penanganan error yang lebih baik Widget _buildImageWidget(String? filename) { if (filename == null || filename.isEmpty) { - return Text("Tidak ada gambar tersedia"); + return _buildPlaceholderImage( + "Tidak ada gambar tersedia", + Icons.image_not_supported, + ); } return FutureBuilder( future: ApiService().getHamaImageBytesByFilename(filename), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); + return SizedBox( + height: 200, + width: double.infinity, + child: Center(child: CircularProgressIndicator()), + ); } else if (snapshot.hasError || snapshot.data == null) { - return Text("Gagal memuat gambar"); + return _buildPlaceholderImage( + "Gagal memuat gambar", + Icons.broken_image, + ); } else { - return Image.memory(snapshot.data!, fit: BoxFit.cover); + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + snapshot.data!, + height: 200, + width: double.infinity, + fit: BoxFit.contain, // untuk memastikan proporsional & penuh + ), + ); } }, ); -} - + } // Widget untuk placeholder gambar Widget _buildPlaceholderImage(String message, IconData icon) { diff --git a/frontend/lib/user/detail_penyakit_page.dart b/frontend/lib/user/detail_penyakit_page.dart index 3a110b9..90e1ecf 100644 --- a/frontend/lib/user/detail_penyakit_page.dart +++ b/frontend/lib/user/detail_penyakit_page.dart @@ -1,9 +1,122 @@ import 'package:flutter/material.dart'; +import 'package:frontend/api_services/api_services.dart'; +import 'dart:typed_data'; -class DetailPenyakitPage extends StatelessWidget { - final Map detailPenyakit; +class DetailPenyakitPage extends StatefulWidget { + final Map DetailPenyakit; + final int? penyakitId; - const DetailPenyakitPage({required this.detailPenyakit}); + const DetailPenyakitPage({Key? key, required this.DetailPenyakit, this.penyakitId}) + : super(key: key); + + @override + _DetailPenyakitPageState createState() => _DetailPenyakitPageState(); +} + +class _DetailPenyakitPageState extends State { + late Future> _detailPenyakitFuture; + late Map _currentDetailPenyakit; + + @override + void initState() { + super.initState(); + _currentDetailPenyakit = widget.DetailPenyakit; + + // Jika hamaId tersedia, fetch data terbaru dari API + if (widget.penyakitId != null) { + _detailPenyakitFuture = _fetchDetailPenyakit(widget.penyakitId!); + } else { + // Jika tidak ada ID, gunakan data yang sudah diberikan + _detailPenyakitFuture = Future.value(widget.DetailPenyakit); + } + } + + Future> _fetchDetailPenyakit(int id) async { + try { + final detailData = await ApiService().getPenyakitById(id); + setState(() { + _currentDetailPenyakit = detailData; + }); + return detailData; + } catch (e) { + print('Error fetching detail penyakit: $e'); + // Jika gagal fetch, gunakan data yang sudah ada + return widget.DetailPenyakit; + } + } + + // Fungsi untuk memvalidasi URL gambar + bool _isValidImageUrl(String? url) { + if (url == null || url.isEmpty) return false; + + // Periksa apakah URL berakhir dengan ekstensi gambar yang umum + final validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + return validExtensions.any((ext) => url.toLowerCase().endsWith(ext)); + } + + // Widget untuk menampilkan gambar dengan penanganan error yang lebih baik + Widget _buildImageWidget(String? filename) { + if (filename == null || filename.isEmpty) { + return _buildPlaceholderImage( + "Tidak ada gambar tersedia", + Icons.image_not_supported, + ); + } + + return FutureBuilder( + future: ApiService().getPenyakitImageBytesByFilename(filename), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SizedBox( + height: 200, + width: double.infinity, + child: Center(child: CircularProgressIndicator()), + ); + } else if (snapshot.hasError || snapshot.data == null) { + return _buildPlaceholderImage( + "Gagal memuat gambar", + Icons.broken_image, + ); + } else { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + snapshot.data!, + height: 200, + width: double.infinity, + fit: BoxFit.contain, // untuk memastikan proporsional & penuh + ), + ); + } + }, + ); + } + + // Widget untuk placeholder gambar + Widget _buildPlaceholderImage(String message, IconData icon) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: Colors.grey[600]), + SizedBox(height: 8), + Text( + message, + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } @override Widget build(BuildContext context) { @@ -11,96 +124,123 @@ class DetailPenyakitPage extends StatelessWidget { backgroundColor: Color(0xFF9DC08D), appBar: AppBar( backgroundColor: Color(0xFF9DC08D), - title: Text( - "Detail Penyakit", - style: TextStyle(color: Colors.white), - ), + title: Text("Detail Penyakit", style: TextStyle(color: Colors.white)), leading: IconButton( icon: Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - if (detailPenyakit["gambar"] != null && detailPenyakit["gambar"]!.isNotEmpty) - ClipRRect( + body: FutureBuilder>( + future: _detailPenyakitFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (snapshot.hasError) { + print('Error: ${snapshot.error}'); + // Tampilkan data yang sudah ada jika terjadi error + return _buildDetailContent(_currentDetailPenyakit); + } + print("Snapshot data runtimeType: ${snapshot.data.runtimeType}"); + print("Snapshot data content: ${snapshot.data}"); + + // Jika berhasil fetch data baru, tampilkan data tersebut + final detailData = snapshot.data ?? _currentDetailPenyakit; + return _buildDetailContent(detailData); + }, + ), + ); + } + + Widget _buildDetailContent(Map detailData) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Tampilkan foto dari database dengan penanganan error yang lebih baik + _buildImageWidget(detailData["foto"]), + SizedBox(height: 16), + + // Card Nama Hama + SizedBox( + width: double.infinity, + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - child: Image.asset( - detailPenyakit["gambar"]!, - height: 200, - width: 200, // Biar gambar full lebar - fit: BoxFit.cover, - ), ), - SizedBox(height: 16), - - // Card Nama Penyakit - SizedBox( - width: double.infinity, // Bikin card full lebar - child: Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Nama Penyakit:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nama Penyakit:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 8), - Text( - detailPenyakit["nama"] ?? "Nama penyakit tidak tersedia", - style: TextStyle(fontSize: 16), - ), - ], - ), + ), + SizedBox(height: 8), + Text( + detailData["nama"] ?? "Nama hama tidak tersedia", + style: TextStyle(fontSize: 16), + ), + ], ), ), ), - SizedBox(height: 16), + ), + SizedBox(height: 16), - // Card Deskripsi + Penanganan - SizedBox( - width: double.infinity, // Bikin card full lebar - child: Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Deskripsi:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + // Card Deskripsi + Penanganan + SizedBox( + width: double.infinity, + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Deskripsi:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 8), - Text( - detailPenyakit["deskripsi"] ?? "Deskripsi tidak tersedia", - style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + detailData["deskripsi"] ?? "Deskripsi tidak tersedia", + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 16), + Text( + "Penanganan:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 16), - Text( - "Penanganan:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - detailPenyakit["penanganan"] ?? "Penanganan tidak tersedia", - style: TextStyle(fontSize: 16), - ), - ], - ), + ), + SizedBox(height: 8), + Text( + detailData["penanganan"] ?? "Penanganan tidak tersedia", + style: TextStyle(fontSize: 16), + ), + ], ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/frontend/lib/user/penyakit_page.dart b/frontend/lib/user/penyakit_page.dart index bbfb8d2..2df119f 100644 --- a/frontend/lib/user/penyakit_page.dart +++ b/frontend/lib/user/penyakit_page.dart @@ -68,7 +68,7 @@ class _PenyakitPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => DetailPenyakitPage(detailPenyakit: penyakit), + builder: (context) => DetailPenyakitPage(DetailPenyakit: penyakit), ), ); },