fix: penambahan logika pencocokan gejala apabila hasil perhitungan sama
This commit is contained in:
parent
f2af845287
commit
6c7e1ee53e
|
@ -9,7 +9,7 @@ function calculateBayesProbability(rules, entityType) {
|
||||||
const entityName = entityData.nama;
|
const entityName = entityData.nama;
|
||||||
const entityId = entityType === 'penyakit' ? rules[0].id_penyakit : rules[0].id_hama;
|
const entityId = entityType === 'penyakit' ? rules[0].id_penyakit : rules[0].id_hama;
|
||||||
|
|
||||||
// LANGKAH 1: Mencari nilai semesta P(E|Hi) untuk setiap gejala
|
// Mencari nilai semesta P(E|Hi) untuk setiap gejala
|
||||||
let nilai_semesta = 0;
|
let nilai_semesta = 0;
|
||||||
const gejalaValues = {};
|
const gejalaValues = {};
|
||||||
|
|
||||||
|
@ -18,13 +18,13 @@ function calculateBayesProbability(rules, entityType) {
|
||||||
nilai_semesta += rule.nilai_pakar;
|
nilai_semesta += rule.nilai_pakar;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LANGKAH 2: Mencari hasil bobot P(Hi) untuk setiap gejala
|
// Mencari hasil bobot P(Hi) untuk setiap gejala
|
||||||
const bobotGejala = {};
|
const bobotGejala = {};
|
||||||
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
||||||
bobotGejala[idGejala] = nilai / nilai_semesta;
|
bobotGejala[idGejala] = nilai / nilai_semesta;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LANGKAH 3: Hitung probabilitas H tanpa memandang Evidence P(E|Hi) × P(Hi)
|
// Hitung probabilitas H tanpa memandang Evidence P(E|Hi) × P(Hi)
|
||||||
const probTanpaEvidence = {};
|
const probTanpaEvidence = {};
|
||||||
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
||||||
probTanpaEvidence[idGejala] = nilai * bobotGejala[idGejala];
|
probTanpaEvidence[idGejala] = nilai * bobotGejala[idGejala];
|
||||||
|
@ -36,13 +36,13 @@ function calculateBayesProbability(rules, entityType) {
|
||||||
totalProbTanpaEvidence += nilai;
|
totalProbTanpaEvidence += nilai;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LANGKAH 4: Hitung probabilitas H dengan memandang Evidence P(Hi|E)
|
// Hitung probabilitas H dengan memandang Evidence P(Hi|E)
|
||||||
const probDenganEvidence = {};
|
const probDenganEvidence = {};
|
||||||
for (const [idGejala, nilai] of Object.entries(probTanpaEvidence)) {
|
for (const [idGejala, nilai] of Object.entries(probTanpaEvidence)) {
|
||||||
probDenganEvidence[idGejala] = nilai / totalProbTanpaEvidence;
|
probDenganEvidence[idGejala] = nilai / totalProbTanpaEvidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LANGKAH 5: Hitung Nilai Bayes ∑bayes = ∑(P(E|Hi) × P(Hi|E))
|
// Hitung Nilai Bayes ∑bayes = ∑(P(E|Hi) × P(Hi|E))
|
||||||
let nilaiBayes = 0;
|
let nilaiBayes = 0;
|
||||||
const detailBayes = [];
|
const detailBayes = [];
|
||||||
|
|
||||||
|
@ -68,10 +68,72 @@ function calculateBayesProbability(rules, entityType) {
|
||||||
nilai_semesta: nilai_semesta,
|
nilai_semesta: nilai_semesta,
|
||||||
detail_perhitungan: detailBayes,
|
detail_perhitungan: detailBayes,
|
||||||
nilai_bayes: nilaiBayes,
|
nilai_bayes: nilaiBayes,
|
||||||
probabilitas_persen: nilaiBayes * 100
|
probabilitas_persen: nilaiBayes * 100,
|
||||||
|
jumlah_gejala_cocok: rules.length // Menambahkan jumlah gejala yang cocok
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function untuk mendapatkan total gejala yang tersedia untuk entity
|
||||||
|
async function getTotalGejalaForEntity(entityId, entityType, inputGejala) {
|
||||||
|
try {
|
||||||
|
let totalGejala = 0;
|
||||||
|
|
||||||
|
if (entityType === 'penyakit') {
|
||||||
|
const allRules = await Rule_penyakit.findAll({
|
||||||
|
where: { id_penyakit: entityId }
|
||||||
|
});
|
||||||
|
totalGejala = allRules.length;
|
||||||
|
} else if (entityType === 'hama') {
|
||||||
|
const allRules = await Rule_hama.findAll({
|
||||||
|
where: { id_hama: entityId }
|
||||||
|
});
|
||||||
|
totalGejala = allRules.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalGejala;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting total gejala:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function untuk menyelesaikan ambiguitas
|
||||||
|
async function resolveAmbiguity(candidates, inputGejala) {
|
||||||
|
// Tambahkan informasi total gejala untuk setiap kandidat
|
||||||
|
for (let candidate of candidates) {
|
||||||
|
const entityType = candidate.type;
|
||||||
|
const entityId = entityType === 'penyakit' ? candidate.id_penyakit : candidate.id_hama;
|
||||||
|
|
||||||
|
candidate.total_gejala_entity = await getTotalGejalaForEntity(entityId, entityType, inputGejala);
|
||||||
|
|
||||||
|
// Hitung persentase kesesuaian gejala
|
||||||
|
candidate.persentase_kesesuaian = candidate.total_gejala_entity > 0
|
||||||
|
? (candidate.jumlah_gejala_cocok / candidate.total_gejala_entity) * 100
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Urutkan berdasarkan:
|
||||||
|
// 1. Jumlah gejala yang cocok (descending)
|
||||||
|
// 2. Persentase kesesuaian (descending)
|
||||||
|
// 3. Total gejala entity (ascending - lebih spesifik lebih baik)
|
||||||
|
candidates.sort((a, b) => {
|
||||||
|
// Prioritas 1: Jumlah gejala cocok
|
||||||
|
if (a.jumlah_gejala_cocok !== b.jumlah_gejala_cocok) {
|
||||||
|
return b.jumlah_gejala_cocok - a.jumlah_gejala_cocok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritas 2: Persentase kesesuaian
|
||||||
|
if (Math.abs(a.persentase_kesesuaian - b.persentase_kesesuaian) > 0.01) {
|
||||||
|
return b.persentase_kesesuaian - a.persentase_kesesuaian;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritas 3: Entity dengan total gejala lebih sedikit (lebih spesifik)
|
||||||
|
return a.total_gejala_entity - b.total_gejala_entity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return candidates[0]; // Kembalikan yang terbaik
|
||||||
|
}
|
||||||
|
|
||||||
exports.diagnosa = async (req, res) => {
|
exports.diagnosa = async (req, res) => {
|
||||||
const { gejala } = req.body;
|
const { gejala } = req.body;
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
|
@ -133,18 +195,57 @@ exports.diagnosa = async (req, res) => {
|
||||||
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
||||||
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
||||||
|
|
||||||
// Simpan histori diagnosa jika ada user yang login dan ada hasil diagnosa
|
// ========== PENANGANAN AMBIGUITAS ==========
|
||||||
|
let hasilTertinggi = null;
|
||||||
|
let isAmbiguous = false;
|
||||||
|
let ambiguityResolution = null;
|
||||||
|
|
||||||
|
if (allResults.length > 0) {
|
||||||
|
const nilaiTertinggi = allResults[0].probabilitas_persen;
|
||||||
|
|
||||||
|
// Cari semua hasil dengan nilai probabilitas yang sama dengan yang tertinggi
|
||||||
|
const kandidatTertinggi = allResults.filter(result =>
|
||||||
|
Math.abs(result.probabilitas_persen - nilaiTertinggi) < 0.0001 // Toleransi untuk floating point
|
||||||
|
);
|
||||||
|
|
||||||
|
if (kandidatTertinggi.length > 1) {
|
||||||
|
// Ada ambiguitas - perlu resolusi
|
||||||
|
isAmbiguous = true;
|
||||||
|
console.log(`Ditemukan ${kandidatTertinggi.length} kandidat dengan nilai probabilitas sama: ${nilaiTertinggi}%`);
|
||||||
|
|
||||||
|
// Lakukan resolusi ambiguitas
|
||||||
|
hasilTertinggi = await resolveAmbiguity(kandidatTertinggi, gejala);
|
||||||
|
|
||||||
|
ambiguityResolution = {
|
||||||
|
total_kandidat: kandidatTertinggi.length,
|
||||||
|
metode_resolusi: 'jumlah_gejala_cocok',
|
||||||
|
kandidat: kandidatTertinggi.map(k => ({
|
||||||
|
type: k.type,
|
||||||
|
nama: k.nama,
|
||||||
|
probabilitas_persen: k.probabilitas_persen,
|
||||||
|
jumlah_gejala_cocok: k.jumlah_gejala_cocok,
|
||||||
|
total_gejala_entity: k.total_gejala_entity,
|
||||||
|
persentase_kesesuaian: k.persentase_kesesuaian
|
||||||
|
})),
|
||||||
|
terpilih: {
|
||||||
|
type: hasilTertinggi.type,
|
||||||
|
nama: hasilTertinggi.nama,
|
||||||
|
alasan: `Memiliki ${hasilTertinggi.jumlah_gejala_cocok} gejala cocok dengan kesesuaian ${hasilTertinggi.persentase_kesesuaian?.toFixed(2)}%`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Tidak ada ambiguitas
|
||||||
|
hasilTertinggi = allResults[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simpan histori diagnosa jika ada user yang login dan ada hasil diagnosa
|
// Simpan histori diagnosa jika ada user yang login dan ada hasil diagnosa
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error('ID user tidak ditemukan. Histori tidak dapat disimpan.');
|
console.error('ID user tidak ditemukan. Histori tidak dapat disimpan.');
|
||||||
} else {
|
} else {
|
||||||
const semuaHasil = [...hasilPenyakit, ...hasilHama];
|
const semuaHasil = [...hasilPenyakit, ...hasilHama];
|
||||||
|
|
||||||
if (semuaHasil.length > 0) {
|
if (semuaHasil.length > 0 && hasilTertinggi) {
|
||||||
const hasilTerbesar = semuaHasil.reduce((max, current) => {
|
|
||||||
return current.probabilitas_persen > max.probabilitas_persen ? current : max;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dapatkan waktu saat ini dalam zona waktu Indonesia (GMT+7)
|
// Dapatkan waktu saat ini dalam zona waktu Indonesia (GMT+7)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const jakartaTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // GMT+7 (WIB)
|
const jakartaTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // GMT+7 (WIB)
|
||||||
|
@ -152,14 +253,14 @@ exports.diagnosa = async (req, res) => {
|
||||||
const baseHistoriData = {
|
const baseHistoriData = {
|
||||||
userId: userId, // harus ada
|
userId: userId, // harus ada
|
||||||
tanggal_diagnosa: jakartaTime, // Menggunakan waktu real-time Indonesia
|
tanggal_diagnosa: jakartaTime, // Menggunakan waktu real-time Indonesia
|
||||||
hasil: hasilTerbesar.nilai_bayes, // harus ada, harus tipe FLOAT
|
hasil: hasilTertinggi.nilai_bayes, // harus ada, harus tipe FLOAT
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tambahkan id_penyakit / id_hama jika ada
|
// Tambahkan id_penyakit / id_hama jika ada
|
||||||
if (hasilTerbesar.id_penyakit) {
|
if (hasilTertinggi.id_penyakit) {
|
||||||
baseHistoriData.id_penyakit = hasilTerbesar.id_penyakit;
|
baseHistoriData.id_penyakit = hasilTertinggi.id_penyakit;
|
||||||
} else if (hasilTerbesar.id_hama) {
|
} else if (hasilTertinggi.id_hama) {
|
||||||
baseHistoriData.id_hama = hasilTerbesar.id_hama;
|
baseHistoriData.id_hama = hasilTertinggi.id_hama;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -180,7 +281,6 @@ exports.diagnosa = async (req, res) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Berhasil melakukan diagnosa',
|
message: 'Berhasil melakukan diagnosa',
|
||||||
|
@ -188,7 +288,9 @@ exports.diagnosa = async (req, res) => {
|
||||||
penyakit: sortedPenyakit,
|
penyakit: sortedPenyakit,
|
||||||
hama: sortedHama,
|
hama: sortedHama,
|
||||||
gejala_input: gejala.map(id => parseInt(id)),
|
gejala_input: gejala.map(id => parseInt(id)),
|
||||||
hasil_tertinggi: allResults.length > 0 ? allResults[0] : null
|
hasil_tertinggi: hasilTertinggi,
|
||||||
|
is_ambiguous: isAmbiguous,
|
||||||
|
ambiguity_resolution: ambiguityResolution
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,6 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi untuk mengelompokkan data berdasarkan userId, diagnosa, dan waktu
|
|
||||||
|
|
||||||
// Fungsi untuk mengelompokkan data berdasarkan userId, diagnosa, dan waktu
|
// Fungsi untuk mengelompokkan data berdasarkan userId, diagnosa, dan waktu
|
||||||
List<Map<String, dynamic>> _groupHistoriData(
|
List<Map<String, dynamic>> _groupHistoriData(
|
||||||
List<Map<String, dynamic>> data,
|
List<Map<String, dynamic>> data,
|
||||||
|
@ -164,9 +162,6 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Riwayat Diagnosa'),
|
title: Text('Riwayat Diagnosa'),
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
backgroundColor: Color(0xFF9DC08D),
|
||||||
actions: [
|
|
||||||
IconButton(icon: Icon(Icons.refresh), onPressed: _loadHistoriData),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body:
|
body:
|
||||||
isLoading
|
isLoading
|
||||||
|
@ -251,7 +246,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
||||||
histori['diagnosa'] ??
|
histori['diagnosa'] ??
|
||||||
'Tidak ada diagnosa',
|
'Tidak ada diagnosa',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _getDiagnosaColor(histori),
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -91,6 +91,50 @@ Future<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async
|
||||||
// Panggil API untuk mendapatkan data histori
|
// Panggil API untuk mendapatkan data histori
|
||||||
final historiResponse = await getHistoriDiagnosa(userId);
|
final historiResponse = await getHistoriDiagnosa(userId);
|
||||||
|
|
||||||
|
// Tambahkan: Panggil API untuk mendapatkan data user
|
||||||
|
final userData = await getUserById(userId);
|
||||||
|
final String userName = userData != null ? userData['name'] ?? "User $userId" : "User $userId";
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async {
|
||||||
|
try {
|
||||||
|
// Panggil API untuk mendapatkan data histori
|
||||||
|
final historiResponse = await getHistoriDiagnosa(userId);
|
||||||
|
|
||||||
|
// Perbaiki cara mendapatkan data user
|
||||||
|
final userData = await getUserById(userId);
|
||||||
|
// Pastikan data user ada dan nama diambil dengan benar
|
||||||
|
final String userName = userData != null && userData['name'] != null
|
||||||
|
? userData['name']
|
||||||
|
: "User $userId";
|
||||||
|
|
||||||
|
print("User Data received: $userData"); // Debug log
|
||||||
|
|
||||||
|
// Proses data histori
|
||||||
|
List<Map<String, dynamic>> result = historiResponse.map((histori) {
|
||||||
|
final gejala = histori['gejala'] ?? {};
|
||||||
|
final penyakit = histori['penyakit'] ?? {};
|
||||||
|
final hama = histori['hama'] ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": histori['id'],
|
||||||
|
"userId": histori['userId'],
|
||||||
|
"name": userName, // Menggunakan nama yang sudah diambil
|
||||||
|
"tanggal_diagnosa": histori['tanggal_diagnosa'],
|
||||||
|
"hasil": histori['hasil'],
|
||||||
|
"gejala_nama": gejala['nama'] ?? "Tidak diketahui",
|
||||||
|
"penyakit_nama": penyakit['nama'],
|
||||||
|
"hama_nama": hama['nama'],
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
print("Processed Histori Data with Username: $result"); // Debug log
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
print("Error fetching histori dengan detail: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Proses data histori
|
// Proses data histori
|
||||||
List<Map<String, dynamic>> result = historiResponse.map((histori) {
|
List<Map<String, dynamic>> result = historiResponse.map((histori) {
|
||||||
// Tangani properti null dengan default value
|
// Tangani properti null dengan default value
|
||||||
|
@ -98,19 +142,19 @@ Future<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async
|
||||||
final penyakit = histori['penyakit'] ?? {};
|
final penyakit = histori['penyakit'] ?? {};
|
||||||
final hama = histori['hama'] ?? {};
|
final hama = histori['hama'] ?? {};
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": histori['id'],
|
"id": histori['id'],
|
||||||
"userId": histori['userId'],
|
"userId": histori['userId'],
|
||||||
|
"name": userName, // Tambahkan nama user ke hasil
|
||||||
"tanggal_diagnosa": histori['tanggal_diagnosa'],
|
"tanggal_diagnosa": histori['tanggal_diagnosa'],
|
||||||
"hasil": histori['hasil'],
|
"hasil": histori['hasil'],
|
||||||
"gejala_nama": gejala['nama'] ?? "Tidak diketahui",
|
"gejala_nama": gejala['nama'] ?? "Tidak diketahui",
|
||||||
"penyakit_nama": penyakit['nama'] ,
|
"penyakit_nama": penyakit['nama'],
|
||||||
"hama_nama": hama['nama'] ,
|
"hama_nama": hama['nama'],
|
||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
print("Processed Histori Data: $result");
|
print("Processed Histori Data with Username: $result");
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error fetching histori dengan detail: $e");
|
print("Error fetching histori dengan detail: $e");
|
||||||
|
@ -1182,6 +1226,28 @@ Future<void> deleteUser(int id) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tambahkan fungsi untuk mendapatkan data user berdasarkan ID
|
||||||
|
Future<Map<String, dynamic>?> getUserById(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('$userUrl/$userId'), // Use userUrl instead of baseUrl
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return jsonDecode(response.body); // Direct return as backend sends user object
|
||||||
|
} else {
|
||||||
|
print("Error fetching user data: ${response.statusCode}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception in getUserById: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,10 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
final List<dynamic> hamaList = data['hama'] ?? [];
|
final List<dynamic> hamaList = data['hama'] ?? [];
|
||||||
final Map<String, dynamic>? hasilTertinggi = data['hasil_tertinggi'];
|
final Map<String, dynamic>? hasilTertinggi = data['hasil_tertinggi'];
|
||||||
|
|
||||||
|
// Ambiguity information from backend
|
||||||
|
final bool isAmbiguous = data['is_ambiguous'] ?? false;
|
||||||
|
final Map<String, dynamic>? ambiguityResolution = data['ambiguity_resolution'];
|
||||||
|
|
||||||
// Get the first penyakit and hama (if any)
|
// Get the first penyakit and hama (if any)
|
||||||
Map<String, dynamic>? firstPenyakit =
|
Map<String, dynamic>? firstPenyakit =
|
||||||
penyakitList.isNotEmpty ? penyakitList.first : null;
|
penyakitList.isNotEmpty ? penyakitList.first : null;
|
||||||
|
@ -82,387 +86,208 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
body: Container(
|
body: Container(
|
||||||
color: Color(0xFFEDF1D6),
|
color: Color(0xFFEDF1D6),
|
||||||
child:
|
child: isLoading
|
||||||
isLoading
|
? Center(child: CircularProgressIndicator())
|
||||||
? Center(child: CircularProgressIndicator())
|
: SingleChildScrollView(
|
||||||
: SingleChildScrollView(
|
padding: EdgeInsets.all(16),
|
||||||
padding: EdgeInsets.all(16),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
// Ambiguity notification (if applicable)
|
||||||
// Main result display
|
if (isAmbiguous && ambiguityResolution != null)
|
||||||
_buildDetailedResult(context, firstPenyakit, firstHama),
|
_buildAmbiguityNotification(ambiguityResolution),
|
||||||
|
|
||||||
SizedBox(height: 24),
|
// Main result display - use hasil_tertinggi from backend
|
||||||
|
_buildDetailedResultFromBackend(context, hasilTertinggi),
|
||||||
|
|
||||||
// Selected symptoms section
|
SizedBox(height: 24),
|
||||||
_buildSection(
|
|
||||||
context,
|
// Selected symptoms section
|
||||||
'Gejala yang Dipilih',
|
_buildSection(
|
||||||
widget.gejalaTerpilih.isEmpty
|
context,
|
||||||
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
'Gejala yang Dipilih',
|
||||||
: Card(
|
widget.gejalaTerpilih.isEmpty
|
||||||
|
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
||||||
|
: Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children:
|
children: widget.gejalaTerpilih
|
||||||
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(
|
.map(
|
||||||
(penyakit) => _buildItemCard(
|
(gejala) => Padding(
|
||||||
penyakit,
|
padding: EdgeInsets.symmetric(
|
||||||
'penyakit',
|
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(),
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
|
|
||||||
// Other possible pests section
|
// Other possible diseases section
|
||||||
_buildSection(
|
_buildSection(
|
||||||
context,
|
context,
|
||||||
'Kemungkinan Hama Lainnya',
|
'Kemungkinan Penyakit Lainnya',
|
||||||
hamaList.length <= 1
|
_buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'),
|
||||||
? _buildEmptyResult(
|
),
|
||||||
'Tidak ada kemungkinan hama lainnya',
|
|
||||||
)
|
SizedBox(height: 24),
|
||||||
: Column(
|
|
||||||
children:
|
// Other possible pests section
|
||||||
hamaList
|
_buildSection(
|
||||||
.skip(
|
context,
|
||||||
1,
|
'Kemungkinan Hama Lainnya',
|
||||||
) // Skip the first one as it's already shown
|
_buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'),
|
||||||
.map(
|
),
|
||||||
(hama) => _buildItemCard(hama, 'hama'),
|
],
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchAdditionalData() async {
|
Widget _buildAmbiguityNotification(Map<String, dynamic> ambiguityResolution) {
|
||||||
setState(() {
|
final totalKandidat = ambiguityResolution['total_kandidat'] ?? 0;
|
||||||
isLoading = true;
|
final terpilih = ambiguityResolution['terpilih'] ?? {};
|
||||||
});
|
final alasan = terpilih['alasan'] ?? '';
|
||||||
|
|
||||||
try {
|
return Card(
|
||||||
print('\n=== DEBUG - STARTING DATA FETCH ===');
|
color: Colors.blue.shade50,
|
||||||
print('DEBUG - hasilDiagnosa input: ${widget.hasilDiagnosa}');
|
elevation: 2,
|
||||||
|
margin: EdgeInsets.only(bottom: 16),
|
||||||
// Fetch all disease and pest data
|
child: Padding(
|
||||||
print('DEBUG - Fetching all penyakit and hama data from API...');
|
padding: EdgeInsets.all(16),
|
||||||
semuaPenyakit = await _apiService.getPenyakit();
|
child: Column(
|
||||||
semuaHama = await _apiService.getHama();
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
print('\nDEBUG - API Data Summary:');
|
Row(
|
||||||
print(
|
children: [
|
||||||
'Fetched ${semuaPenyakit.length} penyakit and ${semuaHama.length} hama from API',
|
Icon(
|
||||||
);
|
Icons.info_outline,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
// Get the lists from the diagnosis result
|
size: 24,
|
||||||
List<dynamic> penyakitList = widget.hasilDiagnosa['penyakit'] ?? [];
|
),
|
||||||
List<dynamic> hamaList = widget.hasilDiagnosa['hama'] ?? [];
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
// Process diseases
|
child: Text(
|
||||||
for (var penyakit in penyakitList) {
|
'Resolusi Ambiguitas',
|
||||||
// Make sure the ID exists and convert to string for consistent comparison
|
style: TextStyle(
|
||||||
var penyakitId = penyakit['id_penyakit'];
|
fontSize: 16,
|
||||||
if (penyakitId == null) continue;
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
String penyakitIdStr = penyakitId.toString();
|
),
|
||||||
print('DEBUG - Processing penyakit ID: $penyakitIdStr');
|
),
|
||||||
|
),
|
||||||
// Find the matching disease in our complete list
|
],
|
||||||
var detail = semuaPenyakit.firstWhere(
|
),
|
||||||
(item) => item['id'].toString() == penyakitIdStr,
|
SizedBox(height: 12),
|
||||||
orElse: () => <String, dynamic>{},
|
Text(
|
||||||
);
|
'Ditemukan $totalKandidat kemungkinan dengan nilai probabilitas yang sama. Sistem telah memilih hasil terbaik berdasarkan:',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
if (detail.isNotEmpty) {
|
),
|
||||||
// Convert probabilitas_persen (0-100) to probabilitas (0-1)
|
SizedBox(height: 8),
|
||||||
double probability = 0.0;
|
Container(
|
||||||
if (penyakit.containsKey('probabilitas_persen')) {
|
padding: EdgeInsets.all(12),
|
||||||
probability =
|
decoration: BoxDecoration(
|
||||||
(penyakit['probabilitas_persen'] as num).toDouble() / 100;
|
color: Colors.blue.shade100,
|
||||||
} else if (penyakit.containsKey('nilai_bayes')) {
|
borderRadius: BorderRadius.circular(8),
|
||||||
probability = (penyakit['nilai_bayes'] as num).toDouble();
|
),
|
||||||
}
|
child: Text(
|
||||||
|
'• $alasan',
|
||||||
// Store the complete details with normalized probability
|
style: TextStyle(
|
||||||
penyakitDetails[penyakitIdStr] = {
|
fontSize: 14,
|
||||||
...detail,
|
fontWeight: FontWeight.w500,
|
||||||
'probabilitas': probability,
|
color: Colors.blue.shade800,
|
||||||
'id_penyakit': penyakitIdStr,
|
),
|
||||||
};
|
),
|
||||||
|
),
|
||||||
final nama =
|
SizedBox(height: 8),
|
||||||
penyakitDetails[penyakitIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
Text(
|
||||||
print('DEBUG - Found details for penyakit ID $penyakitIdStr: $nama');
|
'Metode: Analisis kesesuaian gejala',
|
||||||
}
|
style: TextStyle(
|
||||||
}
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
// Process pests
|
color: Colors.grey.shade600,
|
||||||
for (var hama in hamaList) {
|
),
|
||||||
// Make sure the ID exists and convert to string for consistent comparison
|
),
|
||||||
var hamaId = hama['id_hama'];
|
],
|
||||||
if (hamaId == null) continue;
|
),
|
||||||
|
),
|
||||||
String hamaIdStr = hamaId.toString();
|
);
|
||||||
print('DEBUG - Processing hama ID: $hamaIdStr');
|
|
||||||
|
|
||||||
// Find the matching pest in our complete list
|
|
||||||
var detail = semuaHama.firstWhere(
|
|
||||||
(item) => item['id'].toString() == hamaIdStr,
|
|
||||||
orElse: () => <String, dynamic>{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (detail.isNotEmpty) {
|
|
||||||
// Convert probabilitas_persen (0-100) to probabilitas (0-1)
|
|
||||||
double probability = 0.0;
|
|
||||||
if (hama.containsKey('probabilitas_persen')) {
|
|
||||||
probability = (hama['probabilitas_persen'] as num).toDouble() / 100;
|
|
||||||
} else if (hama.containsKey('nilai_bayes')) {
|
|
||||||
probability = (hama['nilai_bayes'] as num).toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the complete details with normalized probability
|
|
||||||
hamaDetails[hamaIdStr] = {
|
|
||||||
...detail,
|
|
||||||
'probabilitas': probability,
|
|
||||||
'id_hama': hamaIdStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
final nama =
|
|
||||||
hamaDetails[hamaIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
|
||||||
print('DEBUG - Found details for hama ID $hamaIdStr: $nama');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error fetching additional data: $e');
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _getCompleteItemData(
|
Widget _buildDetailedResultFromBackend(
|
||||||
Map<String, dynamic> item,
|
|
||||||
String type,
|
|
||||||
) {
|
|
||||||
// Create a new map for the result
|
|
||||||
Map<String, dynamic> result = {...item};
|
|
||||||
|
|
||||||
// Get the ID based on the correct field name from backend
|
|
||||||
var id = type == 'penyakit' ? item['id_penyakit'] : item['id_hama'];
|
|
||||||
|
|
||||||
print('DEBUG - _getCompleteItemData type: $type, id: $id');
|
|
||||||
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') {
|
|
||||||
details = penyakitDetails[idStr];
|
|
||||||
|
|
||||||
// If not found in our cached details, try to find it in the API data
|
|
||||||
if (details == null || details.isEmpty) {
|
|
||||||
print(
|
|
||||||
'DEBUG - No cached details for penyakit ID: $idStr, searching API data...',
|
|
||||||
);
|
|
||||||
details = semuaPenyakit.firstWhere(
|
|
||||||
(p) => p['id'].toString() == idStr,
|
|
||||||
orElse: () => <String, dynamic>{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (details.isNotEmpty) {
|
|
||||||
// Cache for future use
|
|
||||||
penyakitDetails[idStr] = {...details};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (type == 'hama') {
|
|
||||||
details = hamaDetails[idStr];
|
|
||||||
|
|
||||||
// If not found in our cached details, try to find it in the API data
|
|
||||||
if (details == null || details.isEmpty) {
|
|
||||||
print(
|
|
||||||
'DEBUG - No cached details for hama ID: $idStr, searching API data...',
|
|
||||||
);
|
|
||||||
details = semuaHama.firstWhere(
|
|
||||||
(h) => h['id'].toString() == idStr,
|
|
||||||
orElse: () => <String, dynamic>{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (details.isNotEmpty) {
|
|
||||||
// Cache for future use
|
|
||||||
hamaDetails[idStr] = {...details};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have details, merge them with the result
|
|
||||||
if (details != null && details.isNotEmpty) {
|
|
||||||
print('DEBUG - Found details for $type ID $idStr: ${details['nama']}');
|
|
||||||
|
|
||||||
// Calculate probability (convert from percentage if needed)
|
|
||||||
double probability = 0.0;
|
|
||||||
|
|
||||||
// First check our original item
|
|
||||||
if (item.containsKey('probabilitas_persen')) {
|
|
||||||
probability = (item['probabilitas_persen'] as num).toDouble() / 100;
|
|
||||||
} else if (item.containsKey('nilai_bayes')) {
|
|
||||||
probability = (item['nilai_bayes'] as num).toDouble();
|
|
||||||
} else if (item.containsKey('probabilitas')) {
|
|
||||||
probability = _getProbabilitas(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge all the details
|
|
||||||
result = {
|
|
||||||
...details,
|
|
||||||
...result,
|
|
||||||
'probabilitas': probability,
|
|
||||||
// Make sure these IDs are consistent
|
|
||||||
'id': idStr,
|
|
||||||
type == 'penyakit' ? 'id_penyakit' : 'id_hama': idStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
print(
|
|
||||||
'DEBUG - Final data for $type ID $idStr (${result['nama']}): probabilitas=${result['probabilitas']}',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
print('DEBUG - No details found for $type ID $idStr');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailedResult(
|
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Map<String, dynamic>? penyakit,
|
Map<String, dynamic>? hasilTertinggi,
|
||||||
Map<String, dynamic>? hama,
|
|
||||||
) {
|
) {
|
||||||
// If we have no data, show a message
|
// If no result from backend, show empty message
|
||||||
if (penyakit == null && hama == null) {
|
if (hasilTertinggi == null) {
|
||||||
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
|
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which has higher probability
|
// Determine type based on the presence of id fields
|
||||||
bool isPenyakitHigher = false;
|
|
||||||
Map<String, dynamic>? highest;
|
|
||||||
String type = '';
|
String type = '';
|
||||||
|
bool isPenyakit = false;
|
||||||
|
|
||||||
// Log the incoming data to debug
|
if (hasilTertinggi.containsKey('id_penyakit') && hasilTertinggi['id_penyakit'] != null) {
|
||||||
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';
|
type = 'penyakit';
|
||||||
} else if (hama != null) {
|
isPenyakit = true;
|
||||||
highest = hama;
|
} else if (hasilTertinggi.containsKey('id_hama') && hasilTertinggi['id_hama'] != null) {
|
||||||
isPenyakitHigher = false;
|
|
||||||
type = 'hama';
|
type = 'hama';
|
||||||
|
isPenyakit = false;
|
||||||
|
} else {
|
||||||
|
// Fallback: check type field if available
|
||||||
|
type = hasilTertinggi['type'] ?? 'unknown';
|
||||||
|
isPenyakit = type == 'penyakit';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety check
|
// Get the complete data for the result
|
||||||
if (highest == null) {
|
final completeData = _getCompleteItemData(hasilTertinggi, type);
|
||||||
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
|
// Extract the data we need with safe access
|
||||||
final nama = completeData['nama'] ?? 'Tidak diketahui';
|
final nama = completeData['nama'] ?? hasilTertinggi['nama'] ?? 'Tidak diketahui';
|
||||||
final deskripsi = completeData['deskripsi'] ?? 'Tidak tersedia';
|
final deskripsi = completeData['deskripsi'] ?? 'Tidak tersedia';
|
||||||
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
|
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
|
||||||
final foto = completeData['foto'];
|
final foto = completeData['foto'];
|
||||||
final probabilitas = _getProbabilitas(completeData);
|
final probabilitas = _getProbabilitas(hasilTertinggi);
|
||||||
|
|
||||||
|
// Get additional ambiguity info if available
|
||||||
|
final jumlahGejalacocok = hasilTertinggi['jumlah_gejala_cocok'];
|
||||||
|
final totalGejalaEntity = hasilTertinggi['total_gejala_entity'];
|
||||||
|
final persentaseKesesuaian = hasilTertinggi['persentase_kesesuaian'];
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
print('DEBUG - Building detailed result for: $nama');
|
||||||
|
print('DEBUG - Type: $type, isPenyakit: $isPenyakit');
|
||||||
|
print('DEBUG - Probabilitas: $probabilitas');
|
||||||
|
|
||||||
// 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(
|
return Card(
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color:
|
color: isPenyakit ? Colors.red.shade300 : Colors.orange.shade300,
|
||||||
isPenyakitHigher ? Colors.red.shade300 : Colors.orange.shade300,
|
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -474,13 +299,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isPenyakitHigher
|
isPenyakit ? Icons.coronavirus_outlined : Icons.bug_report,
|
||||||
? Icons.coronavirus_outlined
|
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
||||||
: Icons.bug_report,
|
|
||||||
color:
|
|
||||||
isPenyakitHigher
|
|
||||||
? Colors.red.shade700
|
|
||||||
: Colors.orange.shade700,
|
|
||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
|
@ -490,18 +310,50 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color:
|
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
||||||
isPenyakitHigher
|
|
||||||
? Colors.red.shade700
|
|
||||||
: Colors.orange.shade700,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildProbabilityIndicator(probabilitas),
|
_buildProbabilityIndicator(probabilitas),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Additional info if ambiguity resolution occurred
|
||||||
|
if (jumlahGejalacocok != null && totalGejalaEntity != null)
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 8),
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.analytics_outlined, size: 16, color: Colors.grey.shade600),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (persentaseKesesuaian != null)
|
||||||
|
Text(
|
||||||
|
' (${persentaseKesesuaian.toStringAsFixed(1)}%)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
Divider(thickness: 1, height: 24),
|
Divider(thickness: 1, height: 24),
|
||||||
|
|
||||||
|
// Image section
|
||||||
FutureBuilder<Uint8List?>(
|
FutureBuilder<Uint8List?>(
|
||||||
future: ApiService().getPenyakitImageBytesByFilename(
|
future: ApiService().getPenyakitImageBytesByFilename(
|
||||||
foto.toString(),
|
foto.toString(),
|
||||||
|
@ -522,7 +374,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Image.memory(
|
child: Image.memory(
|
||||||
snapshot.data!,
|
snapshot.data!,
|
||||||
fit: BoxFit.contain, // ✅ agar gambar tidak dipotong
|
fit: BoxFit.contain,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 180,
|
height: 180,
|
||||||
),
|
),
|
||||||
|
@ -573,52 +425,261 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(penanganan, style: TextStyle(fontSize: 14)),
|
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) {
|
Widget _buildOtherPossibilities(
|
||||||
// Get the complete data for this item
|
List<dynamic> itemList,
|
||||||
final completeData = _getCompleteItemData(item, type);
|
Map<String, dynamic>? hasilTertinggi,
|
||||||
|
String type,
|
||||||
|
) {
|
||||||
|
if (itemList.isEmpty) {
|
||||||
|
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the top result that's already shown
|
||||||
|
List<dynamic> otherItems = [];
|
||||||
|
|
||||||
|
if (hasilTertinggi != null) {
|
||||||
|
// Get the ID of the top result
|
||||||
|
String? topResultId;
|
||||||
|
if (type == 'penyakit' && hasilTertinggi.containsKey('id_penyakit')) {
|
||||||
|
topResultId = hasilTertinggi['id_penyakit']?.toString();
|
||||||
|
} else if (type == 'hama' && hasilTertinggi.containsKey('id_hama')) {
|
||||||
|
topResultId = hasilTertinggi['id_hama']?.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out the top result
|
||||||
|
otherItems = itemList.where((item) {
|
||||||
|
String? itemId;
|
||||||
|
if (type == 'penyakit') {
|
||||||
|
itemId = item['id_penyakit']?.toString();
|
||||||
|
} else {
|
||||||
|
itemId = item['id_hama']?.toString();
|
||||||
|
}
|
||||||
|
return topResultId == null || itemId != topResultId;
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
// If no top result, skip the first item
|
||||||
|
otherItems = itemList.skip(1).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherItems.isEmpty) {
|
||||||
|
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: otherItems
|
||||||
|
.map((item) => _buildItemCard(item, type))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchAdditionalData() async {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
print('\n=== DEBUG - STARTING DATA FETCH ===');
|
||||||
|
print('DEBUG - hasilDiagnosa input: ${widget.hasilDiagnosa}');
|
||||||
|
|
||||||
|
// Fetch all disease and pest data
|
||||||
|
print('DEBUG - Fetching all penyakit and hama data from API...');
|
||||||
|
semuaPenyakit = await _apiService.getPenyakit();
|
||||||
|
semuaHama = await _apiService.getHama();
|
||||||
|
|
||||||
|
print('\nDEBUG - API Data Summary:');
|
||||||
|
print(
|
||||||
|
'Fetched ${semuaPenyakit.length} penyakit and ${semuaHama.length} hama from API',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the data from the new backend structure
|
||||||
|
final data = widget.hasilDiagnosa['data'] ?? {};
|
||||||
|
final List<dynamic> penyakitList = data['penyakit'] ?? [];
|
||||||
|
final List<dynamic> hamaList = data['hama'] ?? [];
|
||||||
|
|
||||||
|
// Process diseases
|
||||||
|
for (var penyakit in penyakitList) {
|
||||||
|
var penyakitId = penyakit['id_penyakit'];
|
||||||
|
if (penyakitId == null) continue;
|
||||||
|
|
||||||
|
String penyakitIdStr = penyakitId.toString();
|
||||||
|
print('DEBUG - Processing penyakit ID: $penyakitIdStr');
|
||||||
|
|
||||||
|
var detail = semuaPenyakit.firstWhere(
|
||||||
|
(item) => item['id'].toString() == penyakitIdStr,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (detail.isNotEmpty) {
|
||||||
|
double probability = 0.0;
|
||||||
|
if (penyakit.containsKey('probabilitas_persen')) {
|
||||||
|
probability = (penyakit['probabilitas_persen'] as num).toDouble() / 100;
|
||||||
|
} else if (penyakit.containsKey('nilai_bayes')) {
|
||||||
|
probability = (penyakit['nilai_bayes'] as num).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
penyakitDetails[penyakitIdStr] = {
|
||||||
|
...detail,
|
||||||
|
'probabilitas': probability,
|
||||||
|
'id_penyakit': penyakitIdStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
final nama = penyakitDetails[penyakitIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||||
|
print('DEBUG - Found details for penyakit ID $penyakitIdStr: $nama');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pests
|
||||||
|
for (var hama in hamaList) {
|
||||||
|
var hamaId = hama['id_hama'];
|
||||||
|
if (hamaId == null) continue;
|
||||||
|
|
||||||
|
String hamaIdStr = hamaId.toString();
|
||||||
|
print('DEBUG - Processing hama ID: $hamaIdStr');
|
||||||
|
|
||||||
|
var detail = semuaHama.firstWhere(
|
||||||
|
(item) => item['id'].toString() == hamaIdStr,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (detail.isNotEmpty) {
|
||||||
|
double probability = 0.0;
|
||||||
|
if (hama.containsKey('probabilitas_persen')) {
|
||||||
|
probability = (hama['probabilitas_persen'] as num).toDouble() / 100;
|
||||||
|
} else if (hama.containsKey('nilai_bayes')) {
|
||||||
|
probability = (hama['nilai_bayes'] as num).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
hamaDetails[hamaIdStr] = {
|
||||||
|
...detail,
|
||||||
|
'probabilitas': probability,
|
||||||
|
'id_hama': hamaIdStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
final nama = hamaDetails[hamaIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||||
|
print('DEBUG - Found details for hama ID $hamaIdStr: $nama');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching additional data: $e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _getCompleteItemData(
|
||||||
|
Map<String, dynamic> item,
|
||||||
|
String type,
|
||||||
|
) {
|
||||||
|
Map<String, dynamic> result = {...item};
|
||||||
|
|
||||||
|
var id = type == 'penyakit' ? item['id_penyakit'] : item['id_hama'];
|
||||||
|
|
||||||
|
print('DEBUG - _getCompleteItemData type: $type, id: $id');
|
||||||
|
if (id == null) {
|
||||||
|
print('DEBUG - ID is null, returning original item');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String idStr = id.toString();
|
||||||
|
|
||||||
|
Map<String, dynamic>? details;
|
||||||
|
if (type == 'penyakit') {
|
||||||
|
details = penyakitDetails[idStr];
|
||||||
|
|
||||||
|
if (details == null || details.isEmpty) {
|
||||||
|
print('DEBUG - No cached details for penyakit ID: $idStr, searching API data...');
|
||||||
|
details = semuaPenyakit.firstWhere(
|
||||||
|
(p) => p['id'].toString() == idStr,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (details.isNotEmpty) {
|
||||||
|
penyakitDetails[idStr] = {...details};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type == 'hama') {
|
||||||
|
details = hamaDetails[idStr];
|
||||||
|
|
||||||
|
if (details == null || details.isEmpty) {
|
||||||
|
print('DEBUG - No cached details for hama ID: $idStr, searching API data...');
|
||||||
|
details = semuaHama.firstWhere(
|
||||||
|
(h) => h['id'].toString() == idStr,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (details.isNotEmpty) {
|
||||||
|
hamaDetails[idStr] = {...details};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details != null && details.isNotEmpty) {
|
||||||
|
print('DEBUG - Found details for $type ID $idStr: ${details['nama']}');
|
||||||
|
|
||||||
|
double probability = 0.0;
|
||||||
|
|
||||||
|
if (item.containsKey('probabilitas_persen')) {
|
||||||
|
probability = (item['probabilitas_persen'] as num).toDouble() / 100;
|
||||||
|
} else if (item.containsKey('nilai_bayes')) {
|
||||||
|
probability = (item['nilai_bayes'] as num).toDouble();
|
||||||
|
} else if (item.containsKey('probabilitas')) {
|
||||||
|
probability = _getProbabilitas(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
...details,
|
||||||
|
...result,
|
||||||
|
'probabilitas': probability,
|
||||||
|
'id': idStr,
|
||||||
|
type == 'penyakit' ? 'id_penyakit' : 'id_hama': idStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
print('DEBUG - Final data for $type ID $idStr (${result['nama']}): probabilitas=${result['probabilitas']}');
|
||||||
|
} else {
|
||||||
|
print('DEBUG - No details found for $type ID $idStr');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItemCard(Map<String, dynamic> item, String type) {
|
||||||
|
final completeData = _getCompleteItemData(item, type);
|
||||||
final nama = completeData['nama'] ?? 'Tidak diketahui';
|
final nama = completeData['nama'] ?? 'Tidak diketahui';
|
||||||
final probabilitas = _getProbabilitas(completeData);
|
final probabilitas = _getProbabilitas(completeData);
|
||||||
|
|
||||||
|
// Get additional info for display
|
||||||
|
final jumlahGejalacocok = item['jumlah_gejala_cocok'];
|
||||||
|
final totalGejalaEntity = item['total_gejala_entity'];
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
|
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
|
||||||
color:
|
color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
|
||||||
type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
|
|
||||||
),
|
),
|
||||||
title: Text(nama),
|
title: Text(nama),
|
||||||
|
subtitle: jumlahGejalacocok != null && totalGejalaEntity != null
|
||||||
|
? Text(
|
||||||
|
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildProbabilityIndicator(probabilitas),
|
_buildProbabilityIndicator(probabilitas),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
// IconButton(
|
|
||||||
// icon: Icon(Icons.info_outline),
|
|
||||||
// onPressed: () => _showDetailDialog(context, completeData, type),
|
|
||||||
// color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -671,10 +732,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProbabilityIndicator(double value) {
|
Widget _buildProbabilityIndicator(double value) {
|
||||||
final Color indicatorColor =
|
final Color indicatorColor = value > 0.7
|
||||||
value > 0.7
|
? Colors.red
|
||||||
? Colors.red
|
: value > 0.4
|
||||||
: value > 0.4
|
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.green;
|
: Colors.green;
|
||||||
|
|
||||||
|
@ -695,14 +755,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getProbabilitas(Map<String, dynamic>? item) {
|
double _getProbabilitas(Map<String, dynamic>? item) {
|
||||||
// If item is null, return 0.0
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all possible probability field names from the backend
|
|
||||||
if (item.containsKey('probabilitas_persen')) {
|
if (item.containsKey('probabilitas_persen')) {
|
||||||
// Backend sends percentage (0-100), convert to decimal (0-1)
|
|
||||||
var value = item['probabilitas_persen'];
|
var value = item['probabilitas_persen'];
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
return value.toDouble() / 100;
|
return value.toDouble() / 100;
|
||||||
|
@ -711,7 +768,6 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for nilai_bayes (already in 0-1 format)
|
|
||||||
if (item.containsKey('nilai_bayes')) {
|
if (item.containsKey('nilai_bayes')) {
|
||||||
var value = item['nilai_bayes'];
|
var value = item['nilai_bayes'];
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
|
@ -721,7 +777,6 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally check for probabilitas field that might exist in our frontend objects
|
|
||||||
if (item.containsKey('probabilitas')) {
|
if (item.containsKey('probabilitas')) {
|
||||||
var value = item['probabilitas'];
|
var value = item['probabilitas'];
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
|
|
|
@ -309,7 +309,7 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
children: [
|
children: [
|
||||||
// Card box untuk data pengguna
|
// Card box untuk data pengguna
|
||||||
Container(
|
Container(
|
||||||
height: 400,
|
height: 200,
|
||||||
width: 450,
|
width: 450,
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
@ -432,8 +432,6 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
|
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
|
||||||
Divider(color: Colors.black),
|
Divider(color: Colors.black),
|
||||||
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
|
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
|
||||||
Divider(color: Colors.black),
|
|
||||||
_buildProfileItem("Nomor Telepon: ${userData?['nomorTelepon'] ?? '-'}"),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue