fix: penambahan logika pencocokan gejala apabila hasil perhitungan sama

This commit is contained in:
unknown 2025-05-22 19:32:28 +07:00
parent f2af845287
commit 6c7e1ee53e
5 changed files with 630 additions and 414 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] ?? '-'}"),
], ],
); );
} }