penambahan fungsi implementasi data

This commit is contained in:
unknown 2025-05-05 13:01:42 +07:00
parent 90e496d495
commit a61ffed9cd
27 changed files with 1184 additions and 570 deletions

View File

@ -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' });
}
};

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -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) {

View File

@ -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');
}
};

View File

@ -31,6 +31,10 @@ module.exports = (sequelize) => {
foto: {
type: DataTypes.STRING,
allowNull: false,
},
nilai_pakar: {
type: DataTypes.FLOAT,
allowNull: true
}
},
{

View File

@ -31,6 +31,10 @@ module.exports =(sequelize) => {
foto: {
type: DataTypes.STRING,
allowNull: false,
},
nilai_pakar: {
type: DataTypes.FLOAT,
allowNull: true
}
},
{

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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<AdminPage> {
// 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<void> _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<void> _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,
),
),
],
),
),

View File

@ -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<EditHamaPage> {
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<EditHamaPage> {
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<EditHamaPage> {
_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<EditHamaPage> {
_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<void> _updateHama() async {
try {
setState(() {
@ -99,12 +124,18 @@ class _EditHamaPageState extends State<EditHamaPage> {
_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<EditHamaPage> {
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<EditHamaPage> {
),
);
}
}
}

View File

@ -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<EditPenyakitPage> {
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<EditPenyakitPage> {
String? _errorMessage;
bool _isImageLoading = false;
Uint8List? _currentImageBytes;
double _currentNilaiPakar = 0.0;
@override
void initState() {
@ -49,6 +53,8 @@ class _EditPenyakitPageState extends State<EditPenyakitPage> {
_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<EditPenyakitPage> {
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<void> _updatePenyakit() async {
try {
setState(() {
@ -98,12 +119,18 @@ class _EditPenyakitPageState extends State<EditPenyakitPage> {
_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<EditPenyakitPage> {
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(

View File

@ -13,6 +13,8 @@ class EditRulePage extends StatefulWidget {
final List<double> 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<EditRulePage> {
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<EditRulePage> {
@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<EditRulePage> {
child: ListView(
children: [
// Pilih Hama
Text("Pilih Hama"),
DropdownButton<int>(
isExpanded: true,
value: selectedHamaId,
hint: Text('Pilih Hama'),
items:
hamaList.isNotEmpty
? hamaList.map<DropdownMenuItem<int>>((hama) {
return DropdownMenuItem<int>(
value: hama['id'],
child: Text(hama['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedHamaId = value;
});
},
),
SizedBox(height: 16),
if (!showPenyakitOnly) ...[
Text("Pilih Hama"),
DropdownButton<int>(
isExpanded: true,
value: selectedHamaId,
hint: Text('Pilih Hama'),
items:
hamaList.isNotEmpty
? hamaList.map<DropdownMenuItem<int>>((
hama,
) {
return DropdownMenuItem<int>(
value: hama['id'],
child: Text(hama['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedHamaId = value;
});
},
),
SizedBox(height: 16),
],
// Pilih Penyakit
Text("Pilih Penyakit"),
DropdownButton<int>(
isExpanded: true,
value: selectedPenyakitId,
hint: Text('Pilih Penyakit'),
items:
penyakitList.isNotEmpty
? penyakitList.map<DropdownMenuItem<int>>((
penyakit,
) {
return DropdownMenuItem<int>(
value: penyakit['id'],
child: Text(penyakit['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedPenyakitId = value;
});
},
),
SizedBox(height: 16),
if (!showHamaOnly) ...[
Text("Pilih Penyakit"),
DropdownButton<int>(
isExpanded: true,
value: selectedPenyakitId,
hint: Text('Pilih Penyakit'),
items:
penyakitList.isNotEmpty
? penyakitList.map<DropdownMenuItem<int>>((
penyakit,
) {
return DropdownMenuItem<int>(
value: penyakit['id'],
child: Text(penyakit['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedPenyakitId = value;
});
},
),
SizedBox(height: 16),
],
// Pilih Gejala dan Nilai Pakar
Text("Pilih Gejala"),

View File

@ -67,128 +67,6 @@ class _HamaPageState extends State<HamaPage> {
);
}
// 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<String, dynamic> 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<HamaPage> {
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<HamaPage> {
penangananAwal:
hama['penanganan'] ?? '',
gambarUrl: hama['foto'] ?? '',
nilai_pakar: nilaiPakar,
onHamaUpdated:
_fetchHama, // fungsi untuk refresh list setelah update
),

View File

@ -162,6 +162,38 @@ class _PenyakitPageState extends State<PenyakitPage> {
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<PenyakitPage> {
penyakit['penanganan'] ?? '',
gambarUrl:
penyakit['foto'] ?? '',
nilai_pakar: nilaiPakar,
onPenyakitUpdated:
_fetchPenyakit, // fungsi untuk refresh list setelah update
),

View File

@ -176,141 +176,176 @@ class _RulePageState extends State<RulePage> {
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);
},
),
],
),
),
],
);
}),
),
),
),
),
],
),
),
);
}
}
}

View File

@ -19,6 +19,7 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
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<TambahHamaPage> {
namaController.dispose();
deskripsiController.dispose();
penangananController.dispose();
nilaiPakarController.dispose();
super.dispose();
}
Future<void> _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<TambahHamaPage> {
maxLines: 3,
),
SizedBox(height: 15),
TextField(
controller: nilaiPakarController,
decoration: InputDecoration(labelText: 'Nilai Pakar'),
maxLines: 3,
),
SizedBox(height: 15),
Text('Foto'),
(_webImage != null)
? Image.memory(

View File

@ -18,6 +18,7 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
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<TambahPenyakitPage> {
namaController.dispose();
deskripsiController.dispose();
penangananController.dispose();
nilaiPakarController.dispose();
super.dispose();
}
Future<void> _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<TambahPenyakitPage> {
maxLines: 3,
),
SizedBox(height: 15),
TextField(
controller: nilaiPakarController,
decoration: InputDecoration(labelText: 'nilai pakar'),
maxLines: 3,
),
SizedBox(height: 15),
(_webImage != null)
? Image.memory(
_webImage!,

View File

@ -13,6 +13,8 @@ class TambahRulePage extends StatefulWidget {
final List<double> 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<TambahRulePage> {
int? selectedPenyakitId;
List<int?> selectedGejalaIds = [null];
List<double> nilaiPakarList = [0.5];
List<int?> selectedRuleIds = []; // List paralel dengan selectedGejalaIds dan nilaiPakarList
List<int?> 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<TambahRulePage> {
@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<TambahRulePage> {
}
}
// 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<TambahRulePage> {
child: ListView(
children: [
// Pilih Hama
Text("Pilih Hama"),
DropdownButton<int>(
isExpanded: true,
value: selectedHamaId,
hint: Text('Pilih Hama'),
items:
hamaList.isNotEmpty
? hamaList.map<DropdownMenuItem<int>>((hama) {
return DropdownMenuItem<int>(
value: hama['id'],
child: Text(hama['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedHamaId = value;
});
},
),
SizedBox(height: 16),
if (!showPenyakitOnly) ...[
Text("Pilih Hama"),
DropdownButton<int>(
isExpanded: true,
value: selectedHamaId,
hint: Text('Pilih Hama'),
items:
hamaList.isNotEmpty
? hamaList.map<DropdownMenuItem<int>>((
hama,
) {
return DropdownMenuItem<int>(
value: hama['id'],
child: Text(hama['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedHamaId = value;
});
},
),
SizedBox(height: 16),
],
// Pilih Penyakit
Text("Pilih Penyakit"),
DropdownButton<int>(
isExpanded: true,
value: selectedPenyakitId,
hint: Text('Pilih Penyakit'),
items:
penyakitList.isNotEmpty
? penyakitList.map<DropdownMenuItem<int>>((
penyakit,
) {
return DropdownMenuItem<int>(
value: penyakit['id'],
child: Text(penyakit['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
value: null,
child: Text("Data tidak tersedia"),
),
],
onChanged: (value) {
setState(() {
selectedPenyakitId = value;
});
},
),
SizedBox(height: 16),
if (!showHamaOnly) ...[
Text("Pilih Penyakit"),
DropdownButton<int>(
isExpanded: true,
value: selectedPenyakitId,
hint: Text('Pilih Penyakit'),
items:
penyakitList.isNotEmpty
? penyakitList.map<DropdownMenuItem<int>>((
penyakit,
) {
return DropdownMenuItem<int>(
value: penyakit['id'],
child: Text(penyakit['nama']),
);
}).toList()
: [
DropdownMenuItem<int>(
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<TambahRulePage> {
// 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'),

View File

@ -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<List<Map<String, dynamic>>> 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<Uint8List?> 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<Map<String, dynamic>> 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<List<Map<String, dynamic>>> 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<dynamic> 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<Map<String, dynamic>>.from(filteredData);
}
// Jika tidak ada filter role, kembalikan semua data
return List<Map<String, dynamic>>.from(responseData);
} else {
throw Exception("Gagal mengambil data user: ${response.statusCode}");
}
} catch (e) {
print("Error getUsers: $e");
throw Exception("Gagal mengambil data user");
}
}
}

View File

@ -57,23 +57,40 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
// 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<Uint8List?>(
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) {

View File

@ -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<String, dynamic> detailPenyakit;
class DetailPenyakitPage extends StatefulWidget {
final Map<String, dynamic> 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<DetailPenyakitPage> {
late Future<Map<String, dynamic>> _detailPenyakitFuture;
late Map<String, dynamic> _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<Map<String, dynamic>> _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<Uint8List?>(
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<Map<String, dynamic>>(
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<String, dynamic> 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),
),
],
),
),
),
],
),
),
],
),
),
);

View File

@ -68,7 +68,7 @@ class _PenyakitPageState extends State<PenyakitPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPenyakitPage(detailPenyakit: penyakit),
builder: (context) => DetailPenyakitPage(DetailPenyakit: penyakit),
),
);
},