From 6c7e1ee53ee0d45419850c6688aefe31646197c3 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 22 May 2025 19:32:28 +0700 Subject: [PATCH] fix: penambahan logika pencocokan gejala apabila hasil perhitungan sama --- backend/controller/diagnosaController.js | 140 +++- frontend/lib/admin/admin_histori_page.dart | 7 +- frontend/lib/api_services/api_services.dart | 74 +- frontend/lib/user/hasil_diagnosa_page.dart | 819 +++++++++++--------- frontend/lib/user/profile_page.dart | 4 +- 5 files changed, 630 insertions(+), 414 deletions(-) diff --git a/backend/controller/diagnosaController.js b/backend/controller/diagnosaController.js index e60e4e7..12a4afb 100644 --- a/backend/controller/diagnosaController.js +++ b/backend/controller/diagnosaController.js @@ -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 } }); diff --git a/frontend/lib/admin/admin_histori_page.dart b/frontend/lib/admin/admin_histori_page.dart index 6896b59..4bda799 100644 --- a/frontend/lib/admin/admin_histori_page.dart +++ b/frontend/lib/admin/admin_histori_page.dart @@ -64,8 +64,6 @@ class _AdminHistoriPageState extends State { } } - // Fungsi untuk mengelompokkan data berdasarkan userId, diagnosa, dan waktu - // Fungsi untuk mengelompokkan data berdasarkan userId, diagnosa, dan waktu List> _groupHistoriData( List> data, @@ -164,9 +162,6 @@ class _AdminHistoriPageState extends State { 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 { histori['diagnosa'] ?? 'Tidak ada diagnosa', style: TextStyle( - color: _getDiagnosaColor(histori), + fontWeight: FontWeight.w500, ), ), diff --git a/frontend/lib/api_services/api_services.dart b/frontend/lib/api_services/api_services.dart index 49bac1c..b9d1509 100644 --- a/frontend/lib/api_services/api_services.dart +++ b/frontend/lib/api_services/api_services.dart @@ -90,6 +90,50 @@ Future>> fetchHistoriDenganDetail(String userId) async try { // 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>> 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> 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 List> result = historiResponse.map((histori) { @@ -98,19 +142,19 @@ Future>> fetchHistoriDenganDetail(String userId) async 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'] , + "penyakit_nama": penyakit['nama'], + "hama_nama": hama['nama'], }; }).toList(); - print("Processed Histori Data: $result"); + print("Processed Histori Data with Username: $result"); return result; } catch (e) { print("Error fetching histori dengan detail: $e"); @@ -1182,6 +1226,28 @@ Future deleteUser(int id) async { } } +// Tambahkan fungsi untuk mendapatkan data user berdasarkan ID +Future?> 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; + } +} + } diff --git a/frontend/lib/user/hasil_diagnosa_page.dart b/frontend/lib/user/hasil_diagnosa_page.dart index 6092908..a41d451 100644 --- a/frontend/lib/user/hasil_diagnosa_page.dart +++ b/frontend/lib/user/hasil_diagnosa_page.dart @@ -43,6 +43,10 @@ class _HasilDiagnosaPageState extends State { final List penyakitList = data['penyakit'] ?? []; final List hamaList = data['hama'] ?? []; final Map? hasilTertinggi = data['hasil_tertinggi']; + + // Ambiguity information from backend + final bool isAmbiguous = data['is_ambiguous'] ?? false; + final Map? ambiguityResolution = data['ambiguity_resolution']; // Get the first penyakit and hama (if any) Map? firstPenyakit = @@ -82,387 +86,208 @@ class _HasilDiagnosaPageState extends State { ), ), ), - body: Container( color: Color(0xFFEDF1D6), - child: - isLoading - ? Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Main result display - _buildDetailedResult(context, firstPenyakit, firstHama), + child: isLoading + ? Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Ambiguity notification (if applicable) + if (isAmbiguous && ambiguityResolution != null) + _buildAmbiguityNotification(ambiguityResolution), - SizedBox(height: 24), + // Main result display - use hasil_tertinggi from backend + _buildDetailedResultFromBackend(context, hasilTertinggi), - // Selected symptoms section - _buildSection( - context, - 'Gejala yang Dipilih', - widget.gejalaTerpilih.isEmpty - ? _buildEmptyResult('Tidak ada gejala yang dipilih') - : Card( + SizedBox(height: 24), + + // Selected symptoms section + _buildSection( + context, + 'Gejala yang Dipilih', + widget.gejalaTerpilih.isEmpty + ? _buildEmptyResult('Tidak ada gejala yang dipilih') + : Card( elevation: 2, child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: - widget.gejalaTerpilih - .map( - (gejala) => Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Icon( - Icons.check_circle, - color: Colors.green, - size: 18, - ), - SizedBox(width: 8), - Expanded(child: Text(gejala)), - ], - ), - ), - ) - .toList(), - ), - ), - ), - ), - - SizedBox(height: 24), - - // Other possible diseases section - _buildSection( - context, - 'Kemungkinan Penyakit Lainnya', - penyakitList.length <= 1 - ? _buildEmptyResult( - 'Tidak ada kemungkinan penyakit lainnya', - ) - : Column( - children: - penyakitList - .skip( - 1, - ) // Skip the first one as it's already shown + children: widget.gejalaTerpilih .map( - (penyakit) => _buildItemCard( - penyakit, - 'penyakit', + (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), + SizedBox(height: 24), - // Other possible pests section - _buildSection( - context, - 'Kemungkinan Hama Lainnya', - hamaList.length <= 1 - ? _buildEmptyResult( - 'Tidak ada kemungkinan hama lainnya', - ) - : Column( - children: - hamaList - .skip( - 1, - ) // Skip the first one as it's already shown - .map( - (hama) => _buildItemCard(hama, 'hama'), - ) - .toList(), - ), - ), - ], - ), + // Other possible diseases section + _buildSection( + context, + 'Kemungkinan Penyakit Lainnya', + _buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'), + ), + + SizedBox(height: 24), + + // Other possible pests section + _buildSection( + context, + 'Kemungkinan Hama Lainnya', + _buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'), + ), + ], ), + ), ), ); } - Future _fetchAdditionalData() async { - setState(() { - isLoading = true; - }); + Widget _buildAmbiguityNotification(Map 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', - ); - - // Get the lists from the diagnosis result - List penyakitList = widget.hasilDiagnosa['penyakit'] ?? []; - List 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: () => {}, - ); - - 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: () => {}, - ); - - 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; - }); - } - } + 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, + ), + ), + ], + ), + ), + ); } - Map _getCompleteItemData( - Map item, - String type, - ) { - // Create a new map for the result - Map 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? 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: () => {}, - ); - - 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: () => {}, - ); - - 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? penyakit, - Map? hama, + Map? 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? highest; + // Determine type based on the presence of id fields String type = ''; - - // Log the incoming data to debug - print('DEBUG - Incoming penyakit: $penyakit'); - print('DEBUG - Incoming hama: $hama'); - - // Compare probabilities to determine which to show - if (penyakit != null && hama != null) { - double pProbabilitas = _getProbabilitas(penyakit); - double hProbabilitas = _getProbabilitas(hama); - - isPenyakitHigher = pProbabilitas >= hProbabilitas; - highest = isPenyakitHigher ? penyakit : hama; - type = isPenyakitHigher ? 'penyakit' : 'hama'; - } else if (penyakit != null) { - highest = penyakit; - isPenyakitHigher = true; + bool isPenyakit = false; + + 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 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})'); + // Debug log + print('DEBUG - Building detailed result for: $nama'); + print('DEBUG - Type: $type, isPenyakit: $isPenyakit'); + print('DEBUG - Probabilitas: $probabilitas'); + 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 { 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 { 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( future: ApiService().getPenyakitImageBytesByFilename( foto.toString(), @@ -522,7 +374,7 @@ class _HasilDiagnosaPageState extends State { 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 { ), 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 item, String type) { - // Get the complete data for this item - final completeData = _getCompleteItemData(item, type); + Widget _buildOtherPossibilities( + List itemList, + Map? hasilTertinggi, + String type, + ) { + if (itemList.isEmpty) { + return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya'); + } + // Filter out the top result that's already shown + List 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 _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 penyakitList = data['penyakit'] ?? []; + final List 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: () => {}, + ); + + 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: () => {}, + ); + + 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 _getCompleteItemData( + Map item, + String type, + ) { + Map 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? 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: () => {}, + ); + + 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: () => {}, + ); + + 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 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,10 +732,9 @@ class _HasilDiagnosaPageState extends State { } Widget _buildProbabilityIndicator(double value) { - final Color indicatorColor = - value > 0.7 - ? Colors.red - : value > 0.4 + final Color indicatorColor = value > 0.7 + ? Colors.red + : value > 0.4 ? Colors.orange : Colors.green; @@ -695,14 +755,11 @@ class _HasilDiagnosaPageState extends State { } double _getProbabilitas(Map? 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 { } } - // 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 { } } - // Finally check for probabilitas field that might exist in our frontend objects if (item.containsKey('probabilitas')) { var value = item['probabilitas']; if (value is num) { @@ -777,4 +832,4 @@ Widget _buildProbabilityIndicator(double value) { ), ), ); -} +} \ No newline at end of file diff --git a/frontend/lib/user/profile_page.dart b/frontend/lib/user/profile_page.dart index d26f317..1d71588 100644 --- a/frontend/lib/user/profile_page.dart +++ b/frontend/lib/user/profile_page.dart @@ -309,7 +309,7 @@ class _ProfilPageState extends State { 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 { _buildProfileItem("Email: ${userData?['email'] ?? '-'}"), Divider(color: Colors.black), _buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"), - Divider(color: Colors.black), - _buildProfileItem("Nomor Telepon: ${userData?['nomorTelepon'] ?? '-'}"), ], ); }