fix: halaman hasiil diagnosa dan tampil data di profile

This commit is contained in:
unknown 2025-05-08 22:18:39 +07:00
parent a61ffed9cd
commit b0e682a8b5
17 changed files with 1600 additions and 483 deletions

View File

@ -10,6 +10,7 @@ const hamaRoutes = require('./routes/hamaRoutes');
const penyakitRoutes = require('./routes/penyakitRoutes'); const penyakitRoutes = require('./routes/penyakitRoutes');
const ruleRoutes = require('./routes/ruleRoutes'); const ruleRoutes = require('./routes/ruleRoutes');
const ruleHamaRoutes = require('./routes/ruleHamaRoutes'); const ruleHamaRoutes = require('./routes/ruleHamaRoutes');
const diagnosaRoute = require('./routes/diagnosaRoutes');
const swaggerDocs = require('./swagger'); const swaggerDocs = require('./swagger');
dotenv.config(); dotenv.config();
@ -33,6 +34,7 @@ app.use("/api/hama", hamaRoutes);
app.use("/api/penyakit", penyakitRoutes); app.use("/api/penyakit", penyakitRoutes);
app.use("/api/rules_penyakit", ruleRoutes); app.use("/api/rules_penyakit", ruleRoutes);
app.use("/api/rules_hama", ruleHamaRoutes); app.use("/api/rules_hama", ruleHamaRoutes);
app.use("/api/diagnosa", diagnosaRoute);
// Swagger Documentation // Swagger Documentation

View File

@ -8,95 +8,217 @@ exports.diagnosa = async (req, res) => {
} }
try { try {
// ===================== Penyakit ===================== // Mengambil semua data yang dibutuhkan sekaligus
const allGejala = await Gejala.findAll({
where: { id: gejala }
});
// ========== HITUNG TOTAL P(E) UNTUK SEMUA GEJALA ==========
// P(E) seharusnya sama untuk semua penyakit dan hama yang memiliki gejala yang sama
// Object untuk menyimpan P(E) untuk setiap gejala
const evidenceProbabilities = {};
// Hitung P(E) untuk PENYAKIT
for (const idGejala of gejala) {
// Dapatkan semua rule untuk gejala ini di semua penyakit
const penyakitRulesForGejala = await Rule_penyakit.findAll({
where: { id_gejala: idGejala },
include: [{ model: Penyakit, as: 'penyakit' }]
});
let evidenceProbForGejala = 0;
// Hitung P(E) = Σ [P(E|Hi) * P(Hi)] untuk penyakit
for (const rule of penyakitRulesForGejala) {
const pHi = rule.penyakit.nilai_pakar; // P(Hi)
const pEgivenHi = rule.nilai_pakar; // P(E|Hi)
evidenceProbForGejala += pEgivenHi * pHi;
}
// Dapatkan semua rule untuk gejala ini di semua hama
const hamaRulesForGejala = await Rule_hama.findAll({
where: { id_gejala: idGejala },
include: [{ model: Hama, as: 'hama' }]
});
// Hitung P(E) = Σ [P(E|Hi) * P(Hi)] untuk hama
for (const rule of hamaRulesForGejala) {
const pHi = rule.hama.nilai_pakar; // P(Hi)
const pEgivenHi = rule.nilai_pakar; // P(E|Hi)
evidenceProbForGejala += pEgivenHi * pHi;
}
// Simpan P(E) untuk gejala ini
evidenceProbabilities[idGejala] = evidenceProbForGejala;
}
// Hitung total P(E) untuk semua gejala yang diinput
let totalEvidenceProbability = 0;
for (const idGejala of gejala) {
totalEvidenceProbability += evidenceProbabilities[idGejala] || 0;
}
// Pastikan total P(E) tidak nol untuk menghindari division by zero
if (totalEvidenceProbability === 0) {
totalEvidenceProbability = 1.0;
}
// ========== PENYAKIT ==========
const allPenyakitRules = await Rule_penyakit.findAll({ const allPenyakitRules = await Rule_penyakit.findAll({
where: { where: { id_gejala: gejala },
id_gejala: gejala, include: [{ model: Penyakit, as: 'penyakit' }]
},
include: [
{
model: Penyakit,
as: 'penyakit',
},
],
}); });
const penyakitScores = {}; // Mendapatkan semua penyakit unik yang memiliki gejala yang dipilih
const uniquePenyakitIds = [...new Set(allPenyakitRules.map(rule => rule.id_penyakit))];
allPenyakitRules.forEach(rule => { // Hasil perhitungan untuk setiap penyakit
const idPenyakit = rule.id_penyakit; const hasilPenyakit = [];
const nilaiPakarGejala = rule.nilai_pakar; // P(E|H)
const nilaiPakarPenyakit = rule.penyakit.nilai_pakar; // P(H)
if (!penyakitScores[idPenyakit]) { // Hitung untuk setiap penyakit
// === Menginisialisasi: P(E|H) * P(H) === for (const idPenyakit of uniquePenyakitIds) {
penyakitScores[idPenyakit] = { // Filter rules yang berhubungan dengan penyakit ini
penyakit: rule.penyakit.nama, const penyakitRules = allPenyakitRules.filter(rule => rule.id_penyakit === idPenyakit);
total: nilaiPakarGejala * nilaiPakarPenyakit, // ← Rumus Bayes awal
}; if (penyakitRules.length > 0) {
} else { const dataPenyakit = penyakitRules[0].penyakit;
// === Mengalikan P(E|H) berikutnya (jika diasumsikan independen) === const namaPenyakit = dataPenyakit.nama;
penyakitScores[idPenyakit].total *= nilaiPakarGejala; const priorProbability = dataPenyakit.nilai_pakar; // P(H) - prior probability penyakit
// Menghitung P(E|H) untuk setiap gejala
const evidenceGivenHypothesis = {};
for (const rule of penyakitRules) {
evidenceGivenHypothesis[rule.id_gejala] = rule.nilai_pakar; // P(E|H) untuk setiap gejala
}
// Menghitung P(H|E) = [P(E|H) * P(H)] / P(E) untuk semua gejala
let posteriorNumerator = priorProbability; // Inisialisasi dengan P(H)
const evidencesUsed = [];
// Mengalikan dengan nilai P(E|H) untuk setiap gejala yang ada
for (const idGejala of gejala) {
if (evidenceGivenHypothesis[idGejala]) {
posteriorNumerator *= evidenceGivenHypothesis[idGejala];
evidencesUsed.push({
id_gejala: parseInt(idGejala),
P_E_given_H: evidenceGivenHypothesis[idGejala],
nilai_P_E: evidenceProbabilities[idGejala] || 0
});
}
}
// Posterior probability adalah P(H|E)
const posteriorProbability = posteriorNumerator / totalEvidenceProbability;
hasilPenyakit.push({
id_penyakit: idPenyakit,
nama: namaPenyakit,
P_H: priorProbability,
P_E: totalEvidenceProbability,
evidences: evidencesUsed,
posterior_numerator: posteriorNumerator,
posterior_probability: posteriorProbability,
probabilitas: posteriorProbability
});
} }
}); }
// ===================== Hama ===================== // ========== HAMA ==========
const allHamaRules = await Rule_hama.findAll({ const allHamaRules = await Rule_hama.findAll({
where: { where: { id_gejala: gejala },
id_gejala: gejala, include: [{ model: Hama, as: 'hama' }]
},
include: [
{
model: Hama,
as: 'hama',
},
],
}); });
const hamaScores = {}; // Mendapatkan semua hama unik yang memiliki gejala yang dipilih
const uniqueHamaIds = [...new Set(allHamaRules.map(rule => rule.id_hama))];
allHamaRules.forEach(rule => { // Hasil perhitungan untuk setiap hama
const idHama = rule.id_hama; const hasilHama = [];
const nilaiPakarGejala = rule.nilai_pakar; // P(E|H)
const nilaiPakarHama = rule.hama.nilai_pakar; // P(H)
if (!hamaScores[idHama]) { // Hitung untuk setiap hama
// === Menginisialisasi: P(E|H) * P(H) === for (const idHama of uniqueHamaIds) {
hamaScores[idHama] = { // Filter rules yang berhubungan dengan hama ini
hama: rule.hama.nama, const hamaRules = allHamaRules.filter(rule => rule.id_hama === idHama);
total: nilaiPakarGejala * nilaiPakarHama, // ← Rumus Bayes awal
}; if (hamaRules.length > 0) {
} else { const dataHama = hamaRules[0].hama;
// === Mengalikan P(E|H) berikutnya === const namaHama = dataHama.nama;
hamaScores[idHama].total *= nilaiPakarGejala; const priorProbability = dataHama.nilai_pakar; // P(H) - prior probability hama
// Menghitung P(E|H) untuk setiap gejala
const evidenceGivenHypothesis = {};
for (const rule of hamaRules) {
evidenceGivenHypothesis[rule.id_gejala] = rule.nilai_pakar; // P(E|H) untuk setiap gejala
}
// Menghitung P(H|E) = [P(E|H) * P(H)] / P(E) untuk semua gejala
let posteriorNumerator = priorProbability; // Inisialisasi dengan P(H)
const evidencesUsed = [];
// Mengalikan dengan nilai P(E|H) untuk setiap gejala yang ada
for (const idGejala of gejala) {
if (evidenceGivenHypothesis[idGejala]) {
posteriorNumerator *= evidenceGivenHypothesis[idGejala];
evidencesUsed.push({
id_gejala: parseInt(idGejala),
P_E_given_H: evidenceGivenHypothesis[idGejala],
nilai_P_E: evidenceProbabilities[idGejala] || 0
});
}
}
// Posterior probability adalah P(H|E)
const posteriorProbability = posteriorNumerator / totalEvidenceProbability;
hasilHama.push({
id_hama: idHama,
nama: namaHama,
P_H: priorProbability,
P_E: totalEvidenceProbability,
evidences: evidencesUsed,
posterior_numerator: posteriorNumerator,
posterior_probability: posteriorProbability,
probabilitas: posteriorProbability
});
} }
}
// Urutkan hasil berdasarkan probabilitas
const sortedPenyakit = hasilPenyakit.sort((a, b) => b.probabilitas - a.probabilitas);
const sortedHama = hasilHama.sort((a, b) => b.probabilitas - a.probabilitas);
// Buat ringkasan gejala yang dimasukkan
const gejalaSummary = await Gejala.findAll({
where: { id: gejala },
attributes: ['id', 'kode', 'nama']
}); });
// ===================== Normalisasi (opsional) ===================== // Kirim hasil perhitungan sebagai respons
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({ res.json({
input_gejala: gejalaSummary,
total_evidence_probability: totalEvidenceProbability,
evidence_per_gejala: evidenceProbabilities,
penyakit: sortedPenyakit, penyakit: sortedPenyakit,
hama: sortedHama, hama: sortedHama,
detail_perhitungan: {
keterangan: "Menggunakan teorema Bayes: P(H|E) = [P(E|H) * P(H)] / P(E)",
formula: {
P_H: "Prior probability (nilai pakar untuk penyakit/hama)",
P_E_given_H: "Likelihood (nilai pakar untuk gejala terhadap penyakit/hama)",
P_E: "Evidence probability = Σ [P(E|Hi) * P(Hi)] untuk semua hipotesis",
P_H_given_E: "Posterior probability (hasil akhir)"
}
}
}); });
} catch (error) { } catch (error) {
console.error('Error dalam perhitungan Bayes:', error); console.error('Error dalam perhitungan Bayes:', error);
res.status(500).json({ message: 'Terjadi kesalahan dalam proses diagnosa' }); res.status(500).json({
message: 'Terjadi kesalahan dalam proses diagnosa',
error: error.message
});
} }
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -16,7 +16,6 @@ class Rule_hama extends Model {
foreignKey: 'id_hama', foreignKey: 'id_hama',
as: 'hama', as: 'hama',
}); });
} }
} }

View File

@ -1,10 +1,11 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const diagnosaController = require('../controller/diagnosaController'); const { diagnosa } = require('../controller/diagnosaController');
console.log('Diagnosa function:', diagnosa);
/** /**
* @swagger * @swagger
* /api/diagnosa/bayes: * /api/diagnosa:
* post: * post:
* summary: Melakukan diagnosa penyakit dan hama menggunakan Teorema Bayes * summary: Melakukan diagnosa penyakit dan hama menggunakan Teorema Bayes
* tags: [Diagnosa] * tags: [Diagnosa]
@ -28,15 +29,129 @@ const diagnosaController = require('../controller/diagnosaController');
* schema: * schema:
* type: object * type: object
* properties: * properties:
* input_gejala:
* type: array
* description: Daftar gejala yang diinputkan user
* items:
* type: object
* properties:
* id:
* type: integer
* kode:
* type: string
* nama:
* type: string
* hasil_gabungan:
* type: array
* description: Hasil gabungan hama dan penyakit berdasarkan probabilitas tertinggi
* items:
* type: object
* properties:
* type:
* type: string
* enum: [penyakit, hama]
* nama:
* type: string
* probabilitas:
* type: number
* P_H:
* type: number
* description: Prior probability
* P_E:
* type: number
* description: Evidence probability
* penyakit: * penyakit:
* type: object * type: array
* description: Hasil diagnosa penyakit dengan perhitungan Bayes
* items:
* type: object
* properties:
* id_penyakit:
* type: integer
* nama:
* type: string
* P_H:
* type: number
* description: Prior probability penyakit
* P_E:
* type: number
* description: Evidence probability
* evidences:
* type: array
* items:
* type: object
* properties:
* id_gejala:
* type: integer
* P_E_given_H:
* type: number
* description: Likelihood (nilai pakar gejala)
* posterior_numerator:
* type: number
* description: Hasil perkalian P(E|H) * P(H)
* posterior_probability:
* type: number
* description: P(H|E) - Hasil akhir teorema Bayes
* probabilitas:
* type: number
* description: Nilai probabilitas untuk kompatibilitas
* hama: * hama:
* type: array
* description: Hasil diagnosa hama dengan perhitungan Bayes
* items:
* type: object
* properties:
* id_hama:
* type: integer
* nama:
* type: string
* P_H:
* type: number
* description: Prior probability hama
* P_E:
* type: number
* description: Evidence probability
* evidences:
* type: array
* items:
* type: object
* properties:
* id_gejala:
* type: integer
* P_E_given_H:
* type: number
* description: Likelihood (nilai pakar gejala)
* posterior_numerator:
* type: number
* description: Hasil perkalian P(E|H) * P(H)
* posterior_probability:
* type: number
* description: P(H|E) - Hasil akhir teorema Bayes
* probabilitas:
* type: number
* description: Nilai probabilitas untuk kompatibilitas
* detail_perhitungan:
* type: object * type: object
* description: Informasi tentang metode perhitungan yang digunakan
* properties:
* keterangan:
* type: string
* formula:
* type: object
* properties:
* P_H:
* type: string
* P_E_given_H:
* type: string
* P_E:
* type: string
* P_H_given_E:
* type: string
* 400: * 400:
* description: Permintaan tidak valid * description: Permintaan tidak valid
* 500: * 500:
* description: Terjadi kesalahan pada server * description: Terjadi kesalahan pada server
*/ */
router.post('/bayes', diagnosaController.diagnosaBayes); router.post('/', diagnosa);
module.exports = router; module.exports = router;

View File

@ -20,6 +20,10 @@ class _RulePageState extends State<RulePage> {
List<dynamic> rules = []; List<dynamic> rules = [];
bool isLoading = true; bool isLoading = true;
// Pagination variables
int currentPage = 0;
int rowsPerPage = 10;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -61,23 +65,16 @@ class _RulePageState extends State<RulePage> {
'nama_gejala': gejala['nama'], 'nama_gejala': gejala['nama'],
'nama_penyakit': penyakit['nama'], 'nama_penyakit': penyakit['nama'],
'nama_hama': null, 'nama_hama': null,
'nilai_pakar': 'nilai_pakar': rule['nilai_pakar'],
rule['nilai_pakar'], // Menambahkan nilai_pakar dari rule_penyakit
}; };
}), }),
// Mengolah rules hama // Mengolah rules hama
...rulesHama.map((rule) { ...rulesHama.map((rule) {
print(
"Rule id_gejala: ${rule['id_gejala']}, id_hama: ${rule['id_hama']}",
);
// Mencari gejala berdasarkan id // Mencari gejala berdasarkan id
final gejala = gejalaList.firstWhere((item) { final gejala = gejalaList.firstWhere(
print( (item) => item['id'] == rule['id_gejala'],
"Mencocokkan gejala id: ${item['id']} dengan ${rule['id_gejala']}", orElse: () => {'nama': 'TIDAK DITEMUKAN'},
); );
return item['id'] == rule['id_gejala'];
}, orElse: () => {'nama': 'TIDAK DITEMUKAN'});
// Mencari hama berdasarkan id // Mencari hama berdasarkan id
final hama = hamaList.firstWhere( final hama = hamaList.firstWhere(
@ -85,31 +82,15 @@ class _RulePageState extends State<RulePage> {
orElse: () => {'nama': 'TIDAK DITEMUKAN'}, orElse: () => {'nama': 'TIDAK DITEMUKAN'},
); );
print(
"Gejala ditemukan: ${gejala['nama']}, Hama ditemukan: ${hama['nama']}",
);
// Menampilkan isi dari gejalaList dan hamaList untuk debugging
print("Isi gejalaList:");
for (var item in gejalaList) {
print(item);
}
print("Isi hamaList:");
for (var item in hamaList) {
print(item);
}
return { return {
'id': rule['id'], 'id': rule['id'],
'id_gejala': rule['id_gejala'], 'id_gejala': rule['id_gejala'],
'id_penyakit': null, 'id_penyakit': null,
'id_hama': rule['id_hama'], 'id_hama': rule['id_hama'],
'nama_gejala': gejala['nama'], // Memastikan nama gejala ditampilkan 'nama_gejala': gejala['nama'],
'nama_penyakit': null, 'nama_penyakit': null,
'nama_hama': hama['nama'], // Memastikan nama hama ditampilkan 'nama_hama': hama['nama'],
'nilai_pakar': 'nilai_pakar': rule['nilai_pakar'],
rule['nilai_pakar'], // Menambahkan nilai_pakar dari rule_hama
}; };
}), }),
]; ];
@ -119,14 +100,6 @@ class _RulePageState extends State<RulePage> {
}); });
} catch (e) { } catch (e) {
print('Terjadi kesalahan saat memuat data: $e'); print('Terjadi kesalahan saat memuat data: $e');
for (var rule in rules) {
print("Mencari gejala untuk id_gejala: ${rule['id_gejala']}");
var gejala = gejalaList.firstWhere(
(item) => item['id'].toString() == rule['id_gejala'].toString(),
orElse: () => {'nama': 'TIDAK DITEMUKAN'},
);
print("Gejala ditemukan: ${gejala['nama']}");
}
} finally { } finally {
setState(() { setState(() {
isLoading = false; isLoading = false;
@ -140,23 +113,17 @@ class _RulePageState extends State<RulePage> {
// Tentukan fungsi delete berdasarkan isi rule // Tentukan fungsi delete berdasarkan isi rule
if (rule['id_hama'] != null) { if (rule['id_hama'] != null) {
res = await ApiService.deleteRuleHama( res = await ApiService.deleteRuleHama(rule['id']); // Fungsi API untuk delete hama
rule['id'],
); // Fungsi API untuk delete hama
} else if (rule['id_penyakit'] != null) { } else if (rule['id_penyakit'] != null) {
res = await ApiService.deleteRulePenyakit( res = await ApiService.deleteRulePenyakit(rule['id']); // Fungsi API untuk delete penyakit
rule['id'],
); // Fungsi API untuk delete penyakit
} else { } else {
throw Exception( throw Exception("Data rule tidak valid (tidak ada id_hama atau id_penyakit)");
"Data rule tidak valid (tidak ada id_hama atau id_penyakit)",
);
} }
if (res.statusCode == 200) { if (res.statusCode == 200) {
ScaffoldMessenger.of( ScaffoldMessenger.of(context).showSnackBar(
context, SnackBar(content: Text("Rule berhasil dihapus"))
).showSnackBar(SnackBar(content: Text("Rule berhasil dihapus"))); );
fetchRules(); // Refresh data setelah delete fetchRules(); // Refresh data setelah delete
} else { } else {
throw Exception("Gagal menghapus rule"); throw Exception("Gagal menghapus rule");
@ -168,6 +135,18 @@ class _RulePageState extends State<RulePage> {
} }
} }
// Get paginated data
List<dynamic> get paginatedRules {
final startIndex = currentPage * rowsPerPage;
final endIndex = startIndex + rowsPerPage > rules.length ? rules.length : startIndex + rowsPerPage;
if (startIndex >= rules.length) {
return [];
}
return rules.sublist(startIndex, endIndex);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -238,109 +217,130 @@ class _RulePageState extends State<RulePage> {
isLoading isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Expanded( : Expanded(
child: SingleChildScrollView( child: Column(
scrollDirection: Axis.horizontal, children: [
child: DataTable( Expanded(
columns: const [ child: SingleChildScrollView(
DataColumn(label: Text('No')), scrollDirection: Axis.horizontal,
DataColumn(label: Text('Hama / Penyakit')), child: SingleChildScrollView(
DataColumn(label: Text('Gejala')), scrollDirection: Axis.vertical,
DataColumn(label: Text('nilai pakar')), child: DataTable(
DataColumn(label: Text('Aksi')), headingRowColor: MaterialStateProperty.resolveWith<Color>(
], (Set<MaterialState> states) {
rows: List.generate(rules.length, (index) { return Color(0xFF9DC08D); // Apply color to all header rows
final rule = rules[index]; },
final namaKategori =
rule['id_penyakit'] != null
? rule['nama_penyakit'] ?? '-'
: rule['nama_hama'] ?? '-';
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( columns: const [
Row( DataColumn(label: Text('No')),
children: [ DataColumn(label: Text('Hama & Penyakit')),
IconButton( DataColumn(label: Text('Gejala')),
icon: const Icon( DataColumn(label: Text('Nilai Pakar')),
Icons.edit, DataColumn(label: Text('Aksi')),
color: Colors.orange, ],
), rows: List.generate(paginatedRules.length, (index) {
onPressed: () { final rule = paginatedRules[index];
if (rule != null && final displayIndex = currentPage * rowsPerPage + index + 1;
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( final namaKategori = rule['id_penyakit'] != null
context, ? rule['nama_penyakit'] ?? '-'
MaterialPageRoute( : rule['nama_hama'] ?? '-';
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 final isPenyakit = rule['id_penyakit'] != null;
print("Rule data: $rule");
} return DataRow(
}, cells: [
), DataCell(Text(displayIndex.toString())),
IconButton( DataCell(Text(namaKategori)),
icon: const Icon( DataCell(Text(rule['nama_gejala'] ?? '-')),
Icons.delete, DataCell(Text(rule['nilai_pakar']?.toString() ?? '-')),
color: Colors.red, 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);
},
),
],
), ),
onPressed: () {
deleteRule(rule);
},
), ),
], ],
), );
}),
),
),
),
),
// Pagination controls
Container(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
onPressed: currentPage > 0
? () => setState(() => currentPage--)
: null,
),
Text('Halaman ${currentPage + 1} dari ${(rules.length / rowsPerPage).ceil()}'),
IconButton(
icon: Icon(Icons.chevron_right),
onPressed: (currentPage + 1) * rowsPerPage < rules.length
? () => setState(() => currentPage++)
: null,
), ),
], ],
); ),
}), ),
), ],
), ),
), ),
], ],

View File

@ -14,8 +14,47 @@ class ApiService {
static const String rulesPenyakitUrl ='http://localhost:5000/api/rules_penyakit'; static const String rulesPenyakitUrl ='http://localhost:5000/api/rules_penyakit';
static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama'; static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama';
static const String userUrl = 'http://localhost:5000/api/users'; static const String userUrl = 'http://localhost:5000/api/users';
static const String diagnosaUrl = 'http://localhost:5000/api/diagnosa';
static const Duration timeout = Duration(seconds: 15); static const Duration timeout = Duration(seconds: 15);
/// Fungsi untuk mengirim gejala dan menerima hasil diagnosa
// Kirim gejala dan dapatkan hasil diagnosa
Future<Map<String, dynamic>> diagnosa(List<String> gejalIds) async {
// Konversi string ID menjadi integer jika backend Anda membutuhkan integer
List<int> gejalaNumerik = [];
try {
gejalaNumerik = gejalIds.map((id) => int.parse(id)).toList();
} catch (e) {
print("Error saat konversi ID gejala ke integer: $e");
// Jika konversi gagal, gunakan ID string saja
final response = await http.post(
Uri.parse('$diagnosaUrl'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'gejala': gejalIds}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Gagal melakukan diagnosa: ${response.statusCode} - ${response.body}');
}
}
// Jika konversi berhasil, gunakan ID numerik
final response = await http.post(
Uri.parse('$diagnosaUrl'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'gejala': gejalaNumerik}),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Gagal melakukan diagnosa: ${response.statusCode} - ${response.body}');
}
}
// Fungsi Login (dengan perbaikan) // Fungsi Login (dengan perbaikan)
static Future<Map<String, dynamic>> loginUser( static Future<Map<String, dynamic>> loginUser(
String email, String email,

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'hasil_diagnosa_page.dart'; import 'hasil_diagnosa_page.dart';
import 'package:frontend/api_services/api_services.dart'; // Import API service untuk ambil data import 'package:frontend/api_services/api_services.dart';
class DiagnosaPage extends StatefulWidget { class DiagnosaPage extends StatefulWidget {
@override @override
@ -9,171 +9,220 @@ class DiagnosaPage extends StatefulWidget {
class _DiagnosaPageState extends State<DiagnosaPage> { class _DiagnosaPageState extends State<DiagnosaPage> {
List<Map<String, dynamic>> gejalaList = []; List<Map<String, dynamic>> gejalaList = [];
List<String> gejalaTerpilih = []; List<String> gejalaTerpilihIds = []; // Menyimpan ID gejala
List<String> gejalaTerpilihNames = []; // Menyimpan nama gejala untuk tampilan
bool isLoading = true; bool isLoading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchGejala(); // Ambil data dari MySQL saat halaman dibuka fetchGejala();
} }
void fetchGejala() async { void fetchGejala() async {
try { try {
setState(() { setState(() {
isLoading = true; // Tampilkan loading sebelum data diambil isLoading = true;
}); });
List<Map<String, dynamic>> data = await ApiService().getGejala(); List<Map<String, dynamic>> data = await ApiService().getGejala();
setState(() { setState(() {
gejalaList = data; // Simpan data ke dalam state gejalaList = data;
isLoading = false; // Matikan loading setelah data berhasil diambil isLoading = false;
}); });
} catch (e) { } catch (e) {
print("Error mengambil data gejala: $e"); print("Error mengambil data gejala: $e");
setState(() { setState(() {
isLoading = false; // Pastikan loading berhenti meskipun terjadi error isLoading = false;
}); });
}
} }
}
void pilihGejala(String gejalaId, String gejalaName) {
void pilihGejala(String gejala) {
setState(() { setState(() {
if (!gejalaTerpilih.contains(gejala)) { if (!gejalaTerpilihIds.contains(gejalaId)) {
gejalaTerpilih.add(gejala); gejalaTerpilihIds.add(gejalaId);
gejalaTerpilihNames.add(gejalaName);
} }
}); });
} }
void hapusGejala(String gejala) { void hapusGejala(int index) {
setState(() { setState(() {
gejalaTerpilih.remove(gejala); gejalaTerpilihIds.removeAt(index);
gejalaTerpilihNames.removeAt(index);
}); });
} }
@override void prosesHasilDiagnosa() async {
Widget build(BuildContext context) { if (gejalaTerpilihIds.isEmpty) {
return Scaffold( ScaffoldMessenger.of(context).showSnackBar(
backgroundColor: Color(0xFF9DC08D), SnackBar(content: Text('Silakan pilih minimal satu gejala')),
body: SafeArea( );
child: Column( return;
crossAxisAlignment: CrossAxisAlignment.start, }
children: [
Padding( try {
padding: const EdgeInsets.all(8.0), // Tampilkan indikator loading
child: IconButton( showDialog(
icon: Icon(Icons.arrow_back, color: Colors.white), context: context,
onPressed: () { barrierDismissible: false,
Navigator.pop(context); builder: (BuildContext context) {
}, return Center(
), child: CircularProgressIndicator(),
);
},
);
// Panggil API untuk diagnosa
final hasilDiagnosa = await ApiService().diagnosa(gejalaTerpilihIds);
// Tutup dialog loading
Navigator.pop(context);
// Navigasi ke halaman hasil
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HasilDiagnosaPage(
hasilDiagnosa: hasilDiagnosa,
gejalaTerpilih: gejalaTerpilihNames,
), ),
Padding( ),
padding: const EdgeInsets.only(top: 10.0), );
child: Center( } catch (e) {
child: Column( // Tutup dialog loading
mainAxisSize: MainAxisSize.min, Navigator.pop(context);
children: [
Text( ScaffoldMessenger.of(context).showSnackBar(
"Diagnosa Gejala", SnackBar(content: Text('Terjadi kesalahan: $e')),
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), );
), }
Text( }
"Penyakit dan Hama",
style: TextStyle(fontSize: 20, color: Colors.white), @override
), Widget build(BuildContext context) {
], return Scaffold(
backgroundColor: Color(0xFF9DC08D),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
), ),
), ),
), Padding(
Expanded( padding: const EdgeInsets.only(top: 10.0),
child: isLoading child: Center(
? Center(child: CircularProgressIndicator()) // Tampilkan loading saat mengambil data child: Column(
: Center( mainAxisSize: MainAxisSize.min,
child: Card( children: [
margin: EdgeInsets.all(20), Text(
elevation: 5, "Diagnosa Gejala",
shape: RoundedRectangleBorder( style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
borderRadius: BorderRadius.circular(15), ),
), Text(
child: Padding( "Penyakit dan Hama",
padding: const EdgeInsets.all(20.0), style: TextStyle(fontSize: 20, color: Colors.white),
child: Column( ),
mainAxisSize: MainAxisSize.min, ],
children: [ ),
Text( ),
"Pilih gejala yang Anda temukan", ),
textAlign: TextAlign.center, Expanded(
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), child: isLoading
), ? Center(child: CircularProgressIndicator())
SizedBox(height: 20), : Center(
SizedBox( child: Card(
height: 200, // Perbaiki ukuran list margin: EdgeInsets.all(20),
child: gejalaList.isEmpty elevation: 5,
? Center(child: Text("Tidak ada data gejala")) shape: RoundedRectangleBorder(
: ListView.builder( borderRadius: BorderRadius.circular(15),
itemCount: gejalaList.length, ),
itemBuilder: (context, index) { child: Padding(
String namaGejala = (gejalaList[index]['nama'] ?? "Tidak diketahui").toString(); padding: const EdgeInsets.all(20.0),
return ListTile( child: Column(
title: Text(namaGejala), mainAxisSize: MainAxisSize.min,
trailing: Icon(Icons.add_circle, color: Colors.green), children: [
onTap: () => pilihGejala(namaGejala), Text(
); "Pilih gejala yang Anda temukan",
}, textAlign: TextAlign.center,
), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Divider(color: Colors.grey),
SizedBox(
height: 100,
child: SingleChildScrollView(
child: Column(
children: gejalaTerpilih.map((item) {
return ListTile(
title: Text(item, style: TextStyle(color: Colors.black)),
trailing: Icon(Icons.delete, color: Colors.red),
onTap: () => hapusGejala(item),
);
}).toList(),
),
), ),
), SizedBox(height: 20),
SizedBox(height: 20), SizedBox(
SizedBox( height: 200,
width: double.infinity, child: gejalaList.isEmpty
height: 50, ? Center(child: Text("Tidak ada data gejala"))
child: ElevatedButton( : ListView.builder(
style: ElevatedButton.styleFrom( itemCount: gejalaList.length,
backgroundColor: Colors.green, itemBuilder: (context, index) {
shape: RoundedRectangleBorder( String gejalaId = (gejalaList[index]['id'] ?? "").toString();
borderRadius: BorderRadius.circular(15), String namaGejala = (gejalaList[index]['nama'] ?? "Tidak diketahui").toString();
return ListTile(
title: Text(namaGejala),
trailing: Icon(Icons.add_circle, color: Colors.green),
onTap: () => pilihGejala(gejalaId, namaGejala),
);
},
),
),
Divider(color: Colors.grey),
Text(
"Gejala Terpilih",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 5),
SizedBox(
height: 100,
child: gejalaTerpilihNames.isEmpty
? Center(child: Text("Belum ada gejala yang dipilih"))
: SingleChildScrollView(
child: Column(
children: List.generate(gejalaTerpilihNames.length, (index) {
return ListTile(
title: Text(gejalaTerpilihNames[index], style: TextStyle(color: Colors.black)),
trailing: Icon(Icons.delete, color: Colors.red),
onTap: () => hapusGejala(index),
);
}),
),
),
),
SizedBox(
width: double.infinity,
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: prosesHasilDiagnosa,
child: Text(
"Lihat Hasil Diagnosa",
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
), ),
), ),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HasilDiagnosaPage(gejalaTerpilih: gejalaTerpilih),
),
);
},
child: Text(
"Lihat Hasil Diagnosa",
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), );
); }
}
} }

View File

@ -1,113 +1,733 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:frontend/api_services/api_services.dart';
import 'dart:typed_data';
class HasilDiagnosaPage extends StatelessWidget { class HasilDiagnosaPage extends StatefulWidget {
final Map<String, dynamic> hasilDiagnosa;
final List<String> gejalaTerpilih; final List<String> gejalaTerpilih;
// Constructor untuk menerima data gejala dari halaman sebelumnya HasilDiagnosaPage({
HasilDiagnosaPage({required this.gejalaTerpilih}); required this.hasilDiagnosa,
required this.gejalaTerpilih,
});
// Data contoh untuk penyakit, hama, dan cara penanganan @override
final Map<String, Map<String, String>> database = { _HasilDiagnosaPageState createState() => _HasilDiagnosaPageState();
"Daun berlubang": { }
"penyakit": "Ulat Grayak",
"hama": "Ulat Daun", class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
"penanganan": "Gunakan pestisida alami atau buatan untuk mengendalikan ulat daun." // Maps to store additional data fetched for each item
}, Map<String, Map<String, dynamic>> penyakitDetails = {};
"Daun kecoklatan": { Map<String, Map<String, dynamic>> hamaDetails = {};
"penyakit": "Karat Daun", bool isLoading = true;
"hama": "Kumbang Penggerek",
"penanganan": "Semprotkan fungisida untuk karat daun dan gunakan perangkap kumbang." // Create an instance of your ApiService
}, final ApiService _apiService = ApiService();
"Tepi daun keriting": {
"penyakit": "Virus Keriting", List<Map<String, dynamic>> semuaPenyakit = [];
"hama": "Thrips", List<Map<String, dynamic>> semuaHama = [];
"penanganan": "Buang daun yang terinfeksi dan gunakan insektisida untuk mengendalikan thrips."
}, @override
}; void initState() {
super.initState();
_fetchAdditionalData();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Variabel untuk menghitung hasil berdasarkan gejala yang dipilih // Get the first penyakit and hama (if any)
String namaPenyakit = ""; Map<String, dynamic>? firstPenyakit;
String namaHama = ""; Map<String, dynamic>? firstHama;
String caraPenanganan = "";
// Menentukan hasil berdasarkan gejala yang dipilih List<dynamic> penyakitList = widget.hasilDiagnosa['penyakit'] ?? [];
for (var gejala in gejalaTerpilih) { List<dynamic> hamaList = widget.hasilDiagnosa['hama'] ?? [];
if (database.containsKey(gejala)) {
namaPenyakit = database[gejala]?["penyakit"] ?? ""; if (penyakitList.isNotEmpty) {
namaHama = database[gejala]?["hama"] ?? ""; firstPenyakit = penyakitList.first;
caraPenanganan = database[gejala]?["penanganan"] ?? ""; }
break; // Mengambil data untuk gejala pertama yang cocok
} if (hamaList.isNotEmpty) {
firstHama = hamaList.first;
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Hasil Diagnosa"), title: Text('Hasil Diagnosa'),
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFFEDF1D6),
foregroundColor: Color(0xFF40513B),
), ),
body: Center( body: Container(
child: Card( color: Color(0xFFEDF1D6),
margin: EdgeInsets.all(20), child:
elevation: 5, isLoading
shape: RoundedRectangleBorder( ? Center(child: CircularProgressIndicator())
borderRadius: BorderRadius.circular(15), : SingleChildScrollView(
), padding: EdgeInsets.all(16),
child: Padding( child: Column(
padding: const EdgeInsets.all(20.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
mainAxisSize: MainAxisSize.min, // Main result display
children: [ _buildDetailedResult(context, firstPenyakit, firstHama),
// Nama Penyakit dan Hama
Text(
"Nama Penyakit: ${namaPenyakit.isNotEmpty ? namaPenyakit : "Tidak Diketahui"}",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
"Nama Hama: ${namaHama.isNotEmpty ? namaHama : "Tidak Diketahui"}",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
// Berdasarkan skor SizedBox(height: 24),
Text(
"Berdasarkan perhitungan skor yang telah dilakukan maka tanaman Anda terkena:", // Selected symptoms section
textAlign: TextAlign.center, _buildSection(
style: TextStyle(fontSize: 16), context,
), 'Gejala yang Dipilih',
SizedBox(height: 10), widget.gejalaTerpilih.isEmpty
Text( ? _buildEmptyResult('Tidak ada gejala yang dipilih')
namaPenyakit.isNotEmpty ? namaPenyakit : "Tidak Diketahui", : Card(
textAlign: TextAlign.center, elevation: 2,
style: TextStyle( child: Padding(
fontSize: 18, padding: EdgeInsets.all(16),
fontWeight: FontWeight.bold, child: Column(
color: Colors.red, crossAxisAlignment: CrossAxisAlignment.start,
children:
widget.gejalaTerpilih
.map(
(gejala) => Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(
Icons.check_circle,
color: Colors.green,
size: 18,
),
SizedBox(width: 8),
Expanded(child: Text(gejala)),
],
),
),
)
.toList(),
),
),
),
),
SizedBox(height: 24),
// Other possible diseases section
_buildSection(
context,
'Kemungkinan Penyakit Lainnya',
penyakitList.length <= 1
? _buildEmptyResult(
'Tidak ada kemungkinan penyakit lainnya',
)
: Column(
children:
penyakitList
.skip(
1,
) // Skip the first one as it's already shown
.map(
(penyakit) => _buildItemCard(
penyakit,
'penyakit',
),
)
.toList(),
),
),
SizedBox(height: 24),
// Other possible pests section
_buildSection(
context,
'Kemungkinan Hama Lainnya',
hamaList.length <= 1
? _buildEmptyResult(
'Tidak ada kemungkinan hama lainnya',
)
: Column(
children:
hamaList
.skip(
1,
) // Skip the first one as it's already shown
.map(
(hama) => _buildItemCard(hama, 'hama'),
)
.toList(),
),
),
],
), ),
), ),
SizedBox(height: 20), ),
);
}
// Cara Penanganan Future<void> _fetchAdditionalData() async {
Text( setState(() {
"Cara Penanganan:", isLoading = true;
textAlign: TextAlign.center, });
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
try {
print('\n=== DEBUG - STARTING DATA FETCH ===');
print('DEBUG - hasilDiagnosa input: ${widget.hasilDiagnosa}');
// Fetch all disease and pest data first
print('DEBUG - Fetching all penyakit and hama data from API...');
// Fetch all disease and pest data first
semuaPenyakit = await _apiService.getPenyakit();
semuaHama = await _apiService.getHama();
print('\nDEBUG - API Data Summary:');
print(
'Fetched ${semuaPenyakit.length} penyakit and ${semuaHama.length} hama from API',
);
// Debug log what we got from the API
print(
'Fetched ${semuaPenyakit.length} penyakit and ${semuaHama.length} hama from API',
);
// Get the lists from the diagnosis result
List<dynamic> penyakitList = widget.hasilDiagnosa['penyakit'] ?? [];
List<dynamic> hamaList = widget.hasilDiagnosa['hama'] ?? [];
// Process diseases
for (var penyakit in penyakitList) {
// Make sure the ID exists and convert to string for consistent comparison
var penyakitId = penyakit['id'];
if (penyakitId == null) continue;
print('\nDEBUG - Sample penyakit from API:');
String penyakitIdStr = penyakitId.toString();
// Find the matching disease in our complete list
var detail = semuaPenyakit.firstWhere(
(item) => item['id'].toString() == penyakitIdStr,
orElse: () => <String, dynamic>{},
);
if (detail.isNotEmpty) {
// Store the complete details
penyakitDetails[penyakitIdStr] = {...detail};
// Debug log
print(
'Found details for penyakit ID $penyakitIdStr: ${penyakitDetails[penyakitIdStr]}',
);
}
}
// Process pests
for (var hama in hamaList) {
// Make sure the ID exists and convert to string for consistent comparison
var hamaId = hama['id'];
if (hamaId == null) continue;
String hamaIdStr = hamaId.toString();
print('\nDEBUG - Sample hama from API:');
// Find the matching pest in our complete list
var detail = semuaHama.firstWhere(
(item) => item['id'].toString() == hamaIdStr,
orElse: () => <String, dynamic>{},
);
if (detail.isNotEmpty) {
// Store the complete details
hamaDetails[hamaIdStr] = {...detail};
// Debug log
print(
'Found details for hama ID $hamaIdStr: ${hamaDetails[hamaIdStr]}',
);
}
}
} catch (e) {
print('Error fetching additional data: $e');
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
Map<String, dynamic> _getCompleteItemData(
Map<String, dynamic> item,
String type,
) {
// Create a new map for the result
Map<String, dynamic> result = {...item};
// Debug input item
print('DEBUG - _getCompleteItemData input item: $item');
print('DEBUG - _getCompleteItemData type: $type');
// Get the ID and make sure it's a string
var id = type == 'penyakit' ? item['id_penyakit'] : item['id_hama'];
print('DEBUG - ID from item: $id (${id?.runtimeType})');
if (id == null) {
print('DEBUG - ID is null, returning original item');
return result;
}
String idStr = id.toString();
// Get the detailed information based on type
Map<String, dynamic>? details;
if (type == 'penyakit') {
print('DEBUG - Looking for penyakit details with ID: $idStr');
print('DEBUG - Available penyakit keys: ${penyakitDetails.keys}');
details = penyakitDetails[idStr];
print('DEBUG - Found penyakit details: $details');
// Check raw API data if not found
if (details == null || details.isEmpty) {
print('DEBUG - Checking raw API data for penyakit ID: $idStr');
for (var p in semuaPenyakit) {
print(
' Checking penyakit with ID ${p['id']} (${p['id'].runtimeType})',
);
if (p['id'].toString() == idStr) {
print(' MATCH FOUND in raw API data: $p');
details = p;
break;
}
}
}
} else if (type == 'hama') {
print('DEBUG - Looking for hama details with ID: $idStr');
print('DEBUG - Available hama keys: ${hamaDetails.keys}');
details = hamaDetails[idStr];
print('DEBUG - Found hama details: $details');
// Check raw API data if not found
if (details == null || details.isEmpty) {
print('DEBUG - Checking raw API data for hama ID: $idStr');
for (var h in semuaHama) {
print(' Checking hama with ID ${h['id']} (${h['id'].runtimeType})');
if (h['id'].toString() == idStr) {
print(' MATCH FOUND in raw API data: $h');
details = h;
break;
}
}
}
}
// If we have details, merge them with the result
if (details != null && details.isNotEmpty) {
print('DEBUG - Merging details into result:');
print(' Original result before merge: $result');
print(' Details to merge: $details');
// Make sure details have priority but preserve the original probabilitas
double? originalProbabilitas =
result['probabilitas'] is num
? (result['probabilitas'] as num).toDouble()
: null;
// Merge the details
result.addAll(details);
// Restore original probabilitas if it existed
if (originalProbabilitas != null) {
result['probabilitas'] = originalProbabilitas;
}
print('DEBUG - Final merged result: $result');
// Specifically check key fields
print('DEBUG - Key fields in result:');
print(' nama: ${result['nama']} (${result['nama']?.runtimeType})');
print(
' deskripsi: ${result['deskripsi']} (${result['deskripsi']?.runtimeType})',
);
print(
' penanganan: ${result['penanganan']} (${result['penanganan']?.runtimeType})',
);
print(' foto: ${result['foto']} (${result['foto']?.runtimeType})');
// Debug log
print('Complete data for $type ID $idStr: $result');
}
return result;
}
Widget _buildDetailedResult(
BuildContext context,
Map<String, dynamic>? penyakit,
Map<String, dynamic>? hama,
) {
// If we have no data, show a message
if (penyakit == null && hama == null) {
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
}
// Determine which has higher probability
bool isPenyakitHigher = false;
Map<String, dynamic>? highest;
String type = '';
// Log the incoming data to debug
print('DEBUG - Incoming penyakit: $penyakit');
print('DEBUG - Incoming hama: $hama');
// Compare probabilities to determine which to show
if (penyakit != null && hama != null) {
double pProbabilitas = _getProbabilitas(penyakit);
double hProbabilitas = _getProbabilitas(hama);
isPenyakitHigher = pProbabilitas >= hProbabilitas;
highest = isPenyakitHigher ? penyakit : hama;
type = isPenyakitHigher ? 'penyakit' : 'hama';
} else if (penyakit != null) {
highest = penyakit;
isPenyakitHigher = true;
type = 'penyakit';
} else if (hama != null) {
highest = hama;
isPenyakitHigher = false;
type = 'hama';
}
// Safety check
if (highest == null) {
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
}
// Get the complete data for the highest item
final completeData = _getCompleteItemData(highest, type);
// Debug log
print('Detail result using: $completeData');
// Extract the data we need with safe access
final nama = completeData['nama'] ?? 'Tidak diketahui';
final deskripsi = completeData['deskripsi'] ?? 'Tidak tersedia';
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
final foto = completeData['foto'];
final probabilitas = _getProbabilitas(completeData);
// Debug log specific fields that should be displayed
print('DEBUG - nama: $nama (${nama.runtimeType})');
print('DEBUG - deskripsi: $deskripsi (${deskripsi.runtimeType})');
print('DEBUG - penanganan: $penanganan (${penanganan.runtimeType})');
print('DEBUG - foto: $foto (${foto?.runtimeType})');
print('DEBUG - probabilitas: $probabilitas (${probabilitas.runtimeType})');
return Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(
color:
isPenyakitHigher ? Colors.red.shade300 : Colors.orange.shade300,
width: 2,
),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isPenyakitHigher
? Icons.coronavirus_outlined
: Icons.bug_report,
color:
isPenyakitHigher
? Colors.red.shade700
: Colors.orange.shade700,
size: 28,
), ),
SizedBox(height: 10), SizedBox(width: 8),
Text( Expanded(
caraPenanganan.isNotEmpty child: Text(
? caraPenanganan 'Kemungkinan Terbesar: $nama',
: "Tidak ada informasi penanganan yang tersedia.", style: TextStyle(
textAlign: TextAlign.center, fontSize: 18,
style: TextStyle(fontSize: 16), fontWeight: FontWeight.bold,
color:
isPenyakitHigher
? Colors.red.shade700
: Colors.orange.shade700,
),
),
), ),
_buildProbabilityIndicator(probabilitas),
], ],
), ),
Divider(thickness: 1, height: 24),
FutureBuilder<Uint8List?>(
future: ApiService().getPenyakitImageBytesByFilename(
foto.toString(),
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
height: 180,
color: Colors.grey.shade200,
child: Center(child: CircularProgressIndicator()),
);
} else if (snapshot.hasData && snapshot.data != null) {
return Container(
height: 180,
color: Colors.grey.shade200,
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
snapshot.data!,
fit: BoxFit.contain, // agar gambar tidak dipotong
width: double.infinity,
height: 180,
),
),
),
);
} else {
return Container(
height: 180,
color: Colors.grey.shade200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_not_supported,
size: 40,
color: Colors.grey,
),
SizedBox(height: 8),
Text(
'Gambar tidak tersedia',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
}
},
),
SizedBox(height: 16),
// Description
Text(
'Deskripsi',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(deskripsi, style: TextStyle(fontSize: 14)),
SizedBox(height: 16),
// Treatment
Text(
'Penanganan',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(penanganan, style: TextStyle(fontSize: 14)),
SizedBox(height: 16),
// Button to see more details
// Center(
// child: TextButton.icon(
// icon: Icon(Icons.info_outline),
// label: Text('Lihat Detail Lengkap'),
// onPressed: () => _showDetailDialog(context, completeData, type),
// style: TextButton.styleFrom(
// foregroundColor: isPenyakitHigher ? Colors.red.shade700 : Colors.orange.shade700,
// ),
// ),
// ),
],
),
),
);
}
Widget _buildItemCard(Map<String, dynamic> item, String type) {
// Get the complete data for this item
final completeData = _getCompleteItemData(item, type);
final nama = completeData['nama'] ?? 'Tidak diketahui';
final probabilitas = _getProbabilitas(completeData);
return Card(
margin: EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
color:
type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
),
title: Text(nama),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildProbabilityIndicator(probabilitas),
SizedBox(width: 8),
// IconButton(
// icon: Icon(Icons.info_outline),
// onPressed: () => _showDetailDialog(context, completeData, type),
// color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
// ),
],
),
),
);
}
Widget _buildSection(BuildContext context, String title, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF40513B),
),
),
),
SizedBox(height: 10),
content,
],
);
}
Widget _buildEmptyResult(String message) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
message,
style: TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
), ),
), ),
), ),
); );
} }
Widget _buildProbabilityIndicator(double value) {
final Color indicatorColor =
value > 0.7
? Colors.red
: value > 0.4
? Colors.orange
: Colors.green;
return Container(
width: 60,
height: 30,
decoration: BoxDecoration(
color: indicatorColor,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
);
}
double _getProbabilitas(Map<String, dynamic>? item) {
// If item is null or doesn't contain probabilitas, return 0.0
if (item == null ||
!item.containsKey('probabilitas') ||
item['probabilitas'] == null) {
return 0.0;
}
// Handle different types of probabilitas value
var probValue = item['probabilitas'];
if (probValue is double) {
return probValue;
} else if (probValue is int) {
return probValue.toDouble();
} else if (probValue is String) {
return double.tryParse(probValue) ?? 0.0;
} else {
// For any other type, try to convert to string first then parse
try {
return double.tryParse(probValue.toString()) ?? 0.0;
} catch (e) {
print('Error parsing probabilitas: $e');
return 0.0;
}
}
}
}
Widget _buildEmptyResult(String message) {
return Card(
elevation: 2,
child: Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
message,
style: TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
color: Colors.grey[600],
),
),
),
),
);
}
Widget _buildProbabilityIndicator(double value) {
Color color;
if (value >= 0.7) {
color = Colors.red;
} else if (value >= 0.4) {
color = Colors.orange;
} else {
color = Colors.green;
}
return Container(
width: 60,
height: 30,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
);
} }

View File

@ -17,6 +17,7 @@ class _LoginPageState extends State<LoginPage> {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
bool _isLoading = false; bool _isLoading = false;
Future<void> _login() async { Future<void> _login() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@ -53,6 +54,7 @@ class _LoginPageState extends State<LoginPage> {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('token', responseData['token']); await prefs.setString('token', responseData['token']);
await prefs.setString('role', responseData['role']); await prefs.setString('role', responseData['role']);
await prefs.setString('email', email);
// Redirect berdasarkan role // Redirect berdasarkan role
if (responseData['role'] == 'admin') { if (responseData['role'] == 'admin') {

View File

@ -1,12 +1,118 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'login_page.dart'; import 'login_page.dart';
import 'package:frontend/api_services/api_services.dart'; import 'package:frontend/api_services/api_services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class ProfilPage extends StatelessWidget { class ProfilPage extends StatefulWidget {
Future<void> _logout(BuildContext context) async { @override
await ApiService.logoutUser(); _ProfilPageState createState() => _ProfilPageState();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginPage())); }
class _ProfilPageState extends State<ProfilPage> {
// State untuk menyimpan data user
bool isLoading = true;
Map<String, dynamic>? userData;
String? errorMessage;
String? userRole;
@override
void initState() {
super.initState();
// Panggil API untuk mendapatkan data user saat halaman dibuka
if (userData == null) {
_loadUserData();
}
} }
// Fungsi untuk memuat data pengguna yang login
Future<void> _loadUserData() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
// Ambil data user yang sedang login dari SharedPreferences
SharedPreferences prefs = await SharedPreferences.getInstance();
String? email = prefs.getString('email');
String? token = prefs.getString('token');
userRole = prefs.getString('role');
if (email == null || token == null) {
throw Exception('Sesi login tidak ditemukan, silahkan login kembali');
}
// Buat URL untuk endpoint user API
var url = Uri.parse("http://localhost:5000/api/users");
// Kirim permintaan GET dengan token autentikasi
var response = await http.get(
url,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer $token",
},
);
if (response.statusCode == 200) {
// Parse data respons
List<dynamic> users = jsonDecode(response.body);
print("Email login: $email");
print("Data user dari server: $users");
// Cari user dengan email yang sama dengan yang login
Map<String, dynamic>? currentUser;
for (var user in users) {
if (user['email'].toString().toLowerCase() == email.toLowerCase()) {
currentUser = Map<String, dynamic>.from(user);
print("User ditemukan: $currentUser");
break;
}
}
if (currentUser == null) {
print("User dengan email $email tidak ditemukan di response.");
throw Exception('Data pengguna tidak ditemukan');
}
setState(() {
userData = currentUser;
userRole = currentUser?['role']; // safe access
isLoading = false;
print('User dengan email $email tidak ditemukan di response');
});
} else if (response.statusCode == 401) {
// Token tidak valid atau expired
await ApiService.logoutUser(); // Logout user
throw Exception('Sesi habis, silahkan login kembali');
} else {
throw Exception('Gagal mengambil data: ${response.statusCode}');
}
} catch (e) {
setState(() {
isLoading = false;
errorMessage = "Gagal memuat data profil: ${e.toString()}";
});
}
return;
}
Future<void> _logout(BuildContext context) async {
try {
await ApiService.logoutUser();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
);
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Gagal logout: ${e.toString()}")));
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -17,7 +123,7 @@ class ProfilPage extends StatelessWidget {
Align( Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 40.0), // Jarak dari atas layar padding: const EdgeInsets.only(top: 40.0),
child: Column( child: Column(
children: [ children: [
Text( Text(
@ -29,7 +135,7 @@ class ProfilPage extends StatelessWidget {
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 80), // Menambah jarak antara judul dan card box SizedBox(height: 80),
], ],
), ),
), ),
@ -42,7 +148,7 @@ class ProfilPage extends StatelessWidget {
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28), icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
onPressed: () { onPressed: () {
Navigator.pop(context); // Kembali ke halaman sebelumnya Navigator.pop(context);
}, },
), ),
), ),
@ -56,8 +162,8 @@ class ProfilPage extends StatelessWidget {
children: [ children: [
// Card box untuk data pengguna // Card box untuk data pengguna
Container( Container(
height: 400, // Atur tinggi card box height: 400,
width: 450, // Atur lebar card box width: 450,
child: Card( child: Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -65,33 +171,27 @@ class ProfilPage extends StatelessWidget {
elevation: 4, elevation: 4,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child:
crossAxisAlignment: CrossAxisAlignment.start, isLoading
children: [ ? _buildLoadingState()
_buildProfileItem("Nama: John Doe"), : errorMessage != null
Divider(color: Colors.black), // Garis hitam pemisah ? _buildErrorState()
_buildProfileItem("Username: johndoe"), : _buildUserInfoCard(),
Divider(color: Colors.black),
_buildProfileItem("Password: ********"),
Divider(color: Colors.black),
_buildProfileItem("Email: johndoe@gmail.com"),
Divider(color: Colors.black),
_buildProfileItem("Alamat: Jl. Mawar No. 5"),
Divider(color: Colors.black),
_buildProfileItem("Nomor Telepon: 081234567890"),
],
),
), ),
), ),
), ),
SizedBox(height: 30), SizedBox(height: 30),
// Button di bawah Card box untuk update data profil // Button untuk update data profil
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// Aksi ketika button ditekan (misalnya membuka halaman update data) // Aksi ketika button ditekan (misalnya membuka halaman update data)
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Fitur Update Profil Sedang Dikembangkan")), SnackBar(
content: Text(
"Fitur Update Profil Sedang Dikembangkan",
),
),
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -99,7 +199,10 @@ class ProfilPage extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
backgroundColor: Colors.white, backgroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12), padding: EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
), ),
child: Text( child: Text(
"Update Data Profil", "Update Data Profil",
@ -110,7 +213,9 @@ class ProfilPage extends StatelessWidget {
), ),
), ),
), ),
SizedBox(height: 30), SizedBox(height: 30),
// Button untuk logout
ElevatedButton( ElevatedButton(
onPressed: () => _logout(context), onPressed: () => _logout(context),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -118,7 +223,10 @@ class ProfilPage extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
backgroundColor: Colors.white, backgroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12), padding: EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
), ),
child: Text( child: Text(
"Logout", "Logout",
@ -138,16 +246,77 @@ class ProfilPage extends StatelessWidget {
); );
} }
// Widget untuk menampilkan loading spinner
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Color(0xFF9DC08D)),
SizedBox(height: 16),
Text("Memuat data profil..."),
],
),
);
}
// Widget untuk menampilkan pesan error
Widget _buildErrorState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text(
errorMessage ?? "Terjadi kesalahan",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red),
),
SizedBox(height: 16),
TextButton(onPressed: _loadUserData, child: Text("Coba Lagi")),
],
),
);
}
// Widget untuk menampilkan informasi user yang berhasil dimuat
Widget _buildUserInfoCard() {
if (userData == null) {
return Center(child: Text("Data pengguna belum dimuat."));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProfileItem("Nama: ${userData?['name'] ?? '-'}"),
Divider(color: Colors.black),
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
Divider(color: Colors.black),
_buildProfileItem("Password: ${userData?['password'] ?? '-'}"),
Divider(color: Colors.black),
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
Divider(color: Colors.black),
],
);
}
// Fungsi untuk memformat tanggal
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
return "${date.day}/${date.month}/${date.year}";
} catch (e) {
return dateString;
}
}
// Fungsi untuk membuat item dalam Card box // Fungsi untuk membuat item dalam Card box
Widget _buildProfileItem(String text) { Widget _buildProfileItem(String text) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text( child: Text(text, style: TextStyle(fontSize: 18)),
text,
style: TextStyle(
fontSize: 18,
),
),
); );
} }
} }