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 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;
const gejalaValues = {};
@ -18,13 +18,13 @@ function calculateBayesProbability(rules, entityType) {
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 = {};
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
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 = {};
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
probTanpaEvidence[idGejala] = nilai * bobotGejala[idGejala];
@ -36,13 +36,13 @@ function calculateBayesProbability(rules, entityType) {
totalProbTanpaEvidence += nilai;
}
// LANGKAH 4: Hitung probabilitas H dengan memandang Evidence P(Hi|E)
// Hitung probabilitas H dengan memandang Evidence P(Hi|E)
const probDenganEvidence = {};
for (const [idGejala, nilai] of Object.entries(probTanpaEvidence)) {
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;
const detailBayes = [];
@ -68,10 +68,72 @@ function calculateBayesProbability(rules, entityType) {
nilai_semesta: nilai_semesta,
detail_perhitungan: detailBayes,
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) => {
const { gejala } = req.body;
const userId = req.user?.id;
@ -133,18 +195,57 @@ exports.diagnosa = async (req, res) => {
...sortedHama.map(h => ({ type: 'hama', ...h }))
].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
if (!userId) {
console.error('ID user tidak ditemukan. Histori tidak dapat disimpan.');
} else {
const semuaHasil = [...hasilPenyakit, ...hasilHama];
if (semuaHasil.length > 0) {
const hasilTerbesar = semuaHasil.reduce((max, current) => {
return current.probabilitas_persen > max.probabilitas_persen ? current : max;
});
if (semuaHasil.length > 0 && hasilTertinggi) {
// Dapatkan waktu saat ini dalam zona waktu Indonesia (GMT+7)
const now = new Date();
const jakartaTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // GMT+7 (WIB)
@ -152,14 +253,14 @@ exports.diagnosa = async (req, res) => {
const baseHistoriData = {
userId: userId, // harus ada
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
if (hasilTerbesar.id_penyakit) {
baseHistoriData.id_penyakit = hasilTerbesar.id_penyakit;
} else if (hasilTerbesar.id_hama) {
baseHistoriData.id_hama = hasilTerbesar.id_hama;
if (hasilTertinggi.id_penyakit) {
baseHistoriData.id_penyakit = hasilTertinggi.id_penyakit;
} else if (hasilTertinggi.id_hama) {
baseHistoriData.id_hama = hasilTertinggi.id_hama;
}
try {
@ -180,7 +281,6 @@ exports.diagnosa = async (req, res) => {
}
}
return res.status(200).json({
success: true,
message: 'Berhasil melakukan diagnosa',
@ -188,7 +288,9 @@ exports.diagnosa = async (req, res) => {
penyakit: sortedPenyakit,
hama: sortedHama,
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
List<Map<String, dynamic>> _groupHistoriData(
List<Map<String, dynamic>> data,
@ -164,9 +162,6 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
appBar: AppBar(
title: Text('Riwayat Diagnosa'),
backgroundColor: Color(0xFF9DC08D),
actions: [
IconButton(icon: Icon(Icons.refresh), onPressed: _loadHistoriData),
],
),
body:
isLoading
@ -251,7 +246,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
histori['diagnosa'] ??
'Tidak ada diagnosa',
style: TextStyle(
color: _getDiagnosaColor(histori),
fontWeight: FontWeight.w500,
),
),

View File

@ -91,17 +91,34 @@ Future<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async
// Panggil API untuk mendapatkan data histori
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) {
// Tangani properti null dengan default value
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",
@ -110,7 +127,34 @@ Future<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async
};
}).toList();
print("Processed Histori Data: $result");
print("Processed Histori Data with Username: $result"); // Debug log
return result;
} catch (e) {
print("Error fetching histori dengan detail: $e");
return [];
}
}
// Proses data histori
List<Map<String, dynamic>> result = historiResponse.map((histori) {
// Tangani properti null dengan default value
final gejala = histori['gejala'] ?? {};
final penyakit = histori['penyakit'] ?? {};
final hama = histori['hama'] ?? {};
return {
"id": histori['id'],
"userId": histori['userId'],
"name": userName, // Tambahkan nama user ke hasil
"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");
return result;
} catch (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 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)
Map<String, dynamic>? firstPenyakit =
penyakitList.isNotEmpty ? penyakitList.first : null;
@ -82,19 +86,21 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
),
),
),
body: Container(
color: Color(0xFFEDF1D6),
child:
isLoading
child: isLoading
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main result display
_buildDetailedResult(context, firstPenyakit, firstHama),
// Ambiguity notification (if applicable)
if (isAmbiguous && ambiguityResolution != null)
_buildAmbiguityNotification(ambiguityResolution),
// Main result display - use hasil_tertinggi from backend
_buildDetailedResultFromBackend(context, hasilTertinggi),
SizedBox(height: 24),
@ -110,8 +116,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
widget.gejalaTerpilih
children: widget.gejalaTerpilih
.map(
(gejala) => Padding(
padding: EdgeInsets.symmetric(
@ -144,24 +149,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
_buildSection(
context,
'Kemungkinan Penyakit Lainnya',
penyakitList.length <= 1
? _buildEmptyResult(
'Tidak ada kemungkinan penyakit lainnya',
)
: Column(
children:
penyakitList
.skip(
1,
) // Skip the first one as it's already shown
.map(
(penyakit) => _buildItemCard(
penyakit,
'penyakit',
),
)
.toList(),
),
_buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'),
),
SizedBox(height: 24),
@ -170,21 +158,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
_buildSection(
context,
'Kemungkinan Hama Lainnya',
hamaList.length <= 1
? _buildEmptyResult(
'Tidak ada kemungkinan hama lainnya',
)
: Column(
children:
hamaList
.skip(
1,
) // Skip the first one as it's already shown
.map(
(hama) => _buildItemCard(hama, 'hama'),
)
.toList(),
),
_buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'),
),
],
),
@ -193,276 +167,127 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
);
}
Future<void> _fetchAdditionalData() async {
setState(() {
isLoading = true;
});
Widget _buildAmbiguityNotification(Map<String, dynamic> ambiguityResolution) {
final totalKandidat = ambiguityResolution['total_kandidat'] ?? 0;
final terpilih = ambiguityResolution['terpilih'] ?? {};
final alasan = terpilih['alasan'] ?? '';
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',
return Card(
color: Colors.blue.shade50,
elevation: 2,
margin: EdgeInsets.only(bottom: 16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Colors.blue.shade700,
size: 24,
),
SizedBox(width: 8),
Expanded(
child: Text(
'Resolusi Ambiguitas',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
],
),
SizedBox(height: 12),
Text(
'Ditemukan $totalKandidat kemungkinan dengan nilai probabilitas yang sama. Sistem telah memilih hasil terbaik berdasarkan:',
style: TextStyle(fontSize: 14),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$alasan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.blue.shade800,
),
),
),
SizedBox(height: 8),
Text(
'Metode: Analisis kesesuaian gejala',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey.shade600,
),
),
],
),
),
);
// Get the lists from the diagnosis result
List<dynamic> penyakitList = widget.hasilDiagnosa['penyakit'] ?? [];
List<dynamic> hamaList = widget.hasilDiagnosa['hama'] ?? [];
// Process diseases
for (var penyakit in penyakitList) {
// Make sure the ID exists and convert to string for consistent comparison
var penyakitId = penyakit['id_penyakit'];
if (penyakitId == null) continue;
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,
orElse: () => <String, dynamic>{},
);
if (detail.isNotEmpty) {
// Convert probabilitas_persen (0-100) to probabilitas (0-1)
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();
}
// Store the complete details with normalized probability
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) {
// 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(
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(
Widget _buildDetailedResultFromBackend(
BuildContext context,
Map<String, dynamic>? penyakit,
Map<String, dynamic>? hama,
Map<String, dynamic>? hasilTertinggi,
) {
// If we have no data, show a message
if (penyakit == null && hama == null) {
// If no result from backend, show empty message
if (hasilTertinggi == null) {
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
}
// Determine which has higher probability
bool isPenyakitHigher = false;
Map<String, dynamic>? highest;
// Determine type based on the presence of id fields
String type = '';
bool isPenyakit = false;
// Log the incoming data to debug
print('DEBUG - Incoming penyakit: $penyakit');
print('DEBUG - Incoming hama: $hama');
// Compare probabilities to determine which to show
if (penyakit != null && hama != null) {
double pProbabilitas = _getProbabilitas(penyakit);
double hProbabilitas = _getProbabilitas(hama);
isPenyakitHigher = pProbabilitas >= hProbabilitas;
highest = isPenyakitHigher ? penyakit : hama;
type = isPenyakitHigher ? 'penyakit' : 'hama';
} else if (penyakit != null) {
highest = penyakit;
isPenyakitHigher = true;
if (hasilTertinggi.containsKey('id_penyakit') && hasilTertinggi['id_penyakit'] != null) {
type = 'penyakit';
} else if (hama != null) {
highest = hama;
isPenyakitHigher = false;
isPenyakit = true;
} else if (hasilTertinggi.containsKey('id_hama') && hasilTertinggi['id_hama'] != null) {
type = 'hama';
isPenyakit = false;
} else {
// Fallback: check type field if available
type = hasilTertinggi['type'] ?? 'unknown';
isPenyakit = type == 'penyakit';
}
// Safety check
if (highest == null) {
return _buildEmptyResult('Tidak ada hasil diagnosa yang tersedia');
}
// Get the complete data for the highest item
final completeData = _getCompleteItemData(highest, type);
// Debug log
print('Detail result using: $completeData');
// Get the complete data for the result
final completeData = _getCompleteItemData(hasilTertinggi, type);
// 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 penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
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(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(
color:
isPenyakitHigher ? Colors.red.shade300 : Colors.orange.shade300,
color: isPenyakit ? Colors.red.shade300 : Colors.orange.shade300,
width: 2,
),
),
@ -474,13 +299,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
Row(
children: [
Icon(
isPenyakitHigher
? Icons.coronavirus_outlined
: Icons.bug_report,
color:
isPenyakitHigher
? Colors.red.shade700
: Colors.orange.shade700,
isPenyakit ? Icons.coronavirus_outlined : Icons.bug_report,
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
size: 28,
),
SizedBox(width: 8),
@ -490,18 +310,50 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color:
isPenyakitHigher
? Colors.red.shade700
: Colors.orange.shade700,
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
),
),
),
_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),
// Image section
FutureBuilder<Uint8List?>(
future: ApiService().getPenyakitImageBytesByFilename(
foto.toString(),
@ -522,7 +374,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
borderRadius: BorderRadius.circular(8),
child: Image.memory(
snapshot.data!,
fit: BoxFit.contain, // agar gambar tidak dipotong
fit: BoxFit.contain,
width: double.infinity,
height: 180,
),
@ -573,52 +425,261 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
),
SizedBox(height: 8),
Text(penanganan, style: TextStyle(fontSize: 14)),
SizedBox(height: 16),
// Button to see more details
// Center(
// child: TextButton.icon(
// icon: Icon(Icons.info_outline),
// label: Text('Lihat Detail Lengkap'),
// onPressed: () => _showDetailDialog(context, completeData, type),
// style: TextButton.styleFrom(
// foregroundColor: isPenyakitHigher ? Colors.red.shade700 : Colors.orange.shade700,
// ),
// ),
// ),
],
),
),
);
}
Widget _buildItemCard(Map<String, dynamic> item, String type) {
// Get the complete data for this item
final completeData = _getCompleteItemData(item, type);
Widget _buildOtherPossibilities(
List<dynamic> itemList,
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 probabilitas = _getProbabilitas(completeData);
// Get additional info for display
final jumlahGejalacocok = item['jumlah_gejala_cocok'];
final totalGejalaEntity = item['total_gejala_entity'];
return Card(
margin: EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
color:
type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
),
title: Text(nama),
subtitle: jumlahGejalacocok != null && totalGejalaEntity != null
? Text(
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildProbabilityIndicator(probabilitas),
SizedBox(width: 8),
// IconButton(
// icon: Icon(Icons.info_outline),
// onPressed: () => _showDetailDialog(context, completeData, type),
// color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
// ),
],
),
),
@ -671,8 +732,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
}
Widget _buildProbabilityIndicator(double value) {
final Color indicatorColor =
value > 0.7
final Color indicatorColor = value > 0.7
? Colors.red
: value > 0.4
? Colors.orange
@ -695,14 +755,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
}
double _getProbabilitas(Map<String, dynamic>? item) {
// If item is null, return 0.0
if (item == null) {
return 0.0;
}
// Try all possible probability field names from the backend
if (item.containsKey('probabilitas_persen')) {
// Backend sends percentage (0-100), convert to decimal (0-1)
var value = item['probabilitas_persen'];
if (value is num) {
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')) {
var value = item['nilai_bayes'];
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')) {
var value = item['probabilitas'];
if (value is num) {

View File

@ -309,7 +309,7 @@ class _ProfilPageState extends State<ProfilPage> {
children: [
// Card box untuk data pengguna
Container(
height: 400,
height: 200,
width: 450,
child: Card(
shape: RoundedRectangleBorder(
@ -432,8 +432,6 @@ class _ProfilPageState extends State<ProfilPage> {
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
Divider(color: Colors.black),
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
Divider(color: Colors.black),
_buildProfileItem("Nomor Telepon: ${userData?['nomorTelepon'] ?? '-'}"),
],
);
}