From c012b08a5f6dd6b6ded51673a9cbd8416d53c784 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Jun 2025 09:43:11 +0700 Subject: [PATCH] add new function in diagnosaController --- backend/controller/diagnosaController.js | 50 +++++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 2 +- frontend/lib/api_services/api_services.dart | 22 ++++---- frontend/lib/main.dart | 2 +- frontend/lib/user/detail_riwayat_page.dart | 2 +- frontend/lib/user/diagnosa_page.dart | 49 ++++++++++++++---- frontend/lib/user/forgot_password_page.dart | 4 +- frontend/lib/user/hasil_diagnosa_page.dart | 21 ++++++-- frontend/lib/user/login_page.dart | 2 +- frontend/lib/user/profile_page.dart | 43 ++++++++-------- frontend/lib/user/riwayat_diagnosa_page.dart | 6 ++- 11 files changed, 149 insertions(+), 54 deletions(-) diff --git a/backend/controller/diagnosaController.js b/backend/controller/diagnosaController.js index 0f8bb2b..74b5936 100644 --- a/backend/controller/diagnosaController.js +++ b/backend/controller/diagnosaController.js @@ -152,6 +152,36 @@ function filterSingleSymptomPerfectMatch(results) { return filtered; } +// Helper function untuk memprioritaskan berdasarkan jumlah gejala cocok +function prioritizeBySymptomCount(results) { + // Kelompokkan hasil berdasarkan jumlah gejala cocok + const groupedBySymptoms = {}; + results.forEach(result => { + const symptomCount = result.jumlah_gejala_cocok; + if (!groupedBySymptoms[symptomCount]) { + groupedBySymptoms[symptomCount] = []; + } + groupedBySymptoms[symptomCount].push(result); + }); + + // Ambil kelompok dengan jumlah gejala cocok terbanyak + const maxSymptomCount = Math.max(...Object.keys(groupedBySymptoms).map(Number)); + const topSymptomMatches = groupedBySymptoms[maxSymptomCount]; + + // Jika ada lebih dari 1 hasil dengan gejala cocok terbanyak, urutkan berdasarkan probabilitas + const sortedTopMatches = topSymptomMatches.sort((a, b) => { + return b.probabilitas_persen - a.probabilitas_persen; + }); + + console.log(`Memprioritaskan ${sortedTopMatches.length} hasil dengan ${maxSymptomCount} gejala cocok`); + + return { + prioritizedResults: sortedTopMatches, + maxSymptomCount: maxSymptomCount, + totalGroups: Object.keys(groupedBySymptoms).length + }; +} + exports.diagnosa = async (req, res) => { const { gejala } = req.body; const userId = req.user?.id; @@ -236,6 +266,26 @@ exports.diagnosa = async (req, res) => { filterInfo.fallback_to_symptom_count = true; filterInfo.fallback_reason = 'Semua hasil memiliki 100% akurasi dengan hanya 1 gejala cocok'; + } else if (filteredResults.length > 0) { + // ========== PRIORITAS BERDASARKAN JUMLAH GEJALA COCOK ========== + const priorityAnalysis = prioritizeBySymptomCount(filteredResults); + + // Jika ada hasil dengan gejala cocok lebih banyak, prioritaskan mereka + if (priorityAnalysis.totalGroups > 1) { + console.log(`Menggunakan prioritas gejala: ${priorityAnalysis.maxSymptomCount} gejala cocok diprioritaskan`); + finalResults = priorityAnalysis.prioritizedResults; + + filterInfo.symptom_priority_applied = true; + filterInfo.max_symptom_count = priorityAnalysis.maxSymptomCount; + filterInfo.prioritized_count = priorityAnalysis.prioritizedResults.length; + } else { + // Jika semua hasil memiliki jumlah gejala cocok yang sama, urutkan berdasarkan probabilitas + finalResults = filteredResults.sort((a, b) => { + return b.probabilitas_persen - a.probabilitas_persen; + }); + + filterInfo.sorted_by_probability = true; + } } // ========== PENANGANAN AMBIGUITAS ========== diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index 952499c..b9793bc 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ >> getAllHistori() async { Future getHamaImageBytesByFilename(String filename) async { try { - final url = Uri.parse('https://backend-sistem-pakar-diagnosa-penya.vercel.app/image_hama/$filename'); + final url = Uri.parse('https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/image_hama/$filename'); print('Fetching image from: $url'); final response = await http.get(url); @@ -721,7 +721,7 @@ Future>> getAllHistori() async { Future getPenyakitImageBytesByFilename(String filename) async { try { - final url = Uri.parse('https://backend-sistem-pakar-diagnosa-penya.vercel.app/image_penyakit/$filename'); + final url = Uri.parse('https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/image_penyakit/$filename'); print('Fetching image from: $url'); final response = await http.get(url); diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index d1afd1b..eca7c48 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -41,7 +41,7 @@ class _MyAppState extends State { Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, - title: 'Sistem Pakar Bayam', + title: 'SIBAYAM', home: _initialScreen, ); } diff --git a/frontend/lib/user/detail_riwayat_page.dart b/frontend/lib/user/detail_riwayat_page.dart index 611e0b4..d66bc09 100644 --- a/frontend/lib/user/detail_riwayat_page.dart +++ b/frontend/lib/user/detail_riwayat_page.dart @@ -153,7 +153,7 @@ class DetailRiwayatPage extends StatelessWidget { Text('Gejala: $gejalaList', style: TextStyle(fontSize: 16)), SizedBox(height: 8), Text( - 'Hasil: ${(detailRiwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}', + 'Hasil: ${detailRiwayat['hasil'] != null ? "${(((detailRiwayat['hasil'] as num) * 1000).floor() / 10).toStringAsFixed(1)}%" : "-"}', style: TextStyle(fontSize: 16), ), SizedBox(height: 8), diff --git a/frontend/lib/user/diagnosa_page.dart b/frontend/lib/user/diagnosa_page.dart index 7eb8593..645bbf8 100644 --- a/frontend/lib/user/diagnosa_page.dart +++ b/frontend/lib/user/diagnosa_page.dart @@ -56,9 +56,14 @@ class _DiagnosaPageState extends State { } void prosesHasilDiagnosa() async { - if (gejalaTerpilihIds.isEmpty) { + // Validasi minimal 3 gejala + if (gejalaTerpilihIds.length < 3) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Silakan pilih minimal satu gejala')), + SnackBar( + content: Text('Silakan pilih minimal 3 gejala untuk melakukan diagnosa'), + backgroundColor: Color(0xFF9DC08D), + duration: Duration(seconds: 3), + ), ); return; } @@ -175,9 +180,29 @@ class _DiagnosaPageState extends State { ), ), Divider(color: Colors.grey), - Text( - "Gejala Terpilih", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Gejala Terpilih", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: gejalaTerpilihIds.length >= 3 ? Colors.green : Colors.green, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "${gejalaTerpilihIds.length}/min 3", + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), SizedBox(height: 5), SizedBox( @@ -202,15 +227,21 @@ class _DiagnosaPageState extends State { height: 30, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, + backgroundColor: gejalaTerpilihIds.length >= 3 ? Colors.green : Colors.grey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), ), - onPressed: prosesHasilDiagnosa, + onPressed: gejalaTerpilihIds.length >= 3 ? prosesHasilDiagnosa : null, child: Text( - "Lihat Hasil Diagnosa", - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), + gejalaTerpilihIds.length >= 3 + ? "Lihat Hasil Diagnosa" + : "Pilih minimal 3 gejala", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold + ), ), ), ), diff --git a/frontend/lib/user/forgot_password_page.dart b/frontend/lib/user/forgot_password_page.dart index 23e8965..d3d2b2e 100644 --- a/frontend/lib/user/forgot_password_page.dart +++ b/frontend/lib/user/forgot_password_page.dart @@ -257,10 +257,10 @@ class _ForgotPasswordPageState extends State { return; } - if (passwordController.text.length < 6) { + if (passwordController.text.length < 8) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Password minimal 6 karakter.'), + content: Text('Password minimal 8 karakter.'), backgroundColor: Colors.red, ), ); diff --git a/frontend/lib/user/hasil_diagnosa_page.dart b/frontend/lib/user/hasil_diagnosa_page.dart index 7e6173e..086e5a6 100644 --- a/frontend/lib/user/hasil_diagnosa_page.dart +++ b/frontend/lib/user/hasil_diagnosa_page.dart @@ -614,6 +614,12 @@ class _HasilDiagnosaPageState extends State { return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya'); } + // Get the probability of the main diagnosis result + double? mainDiagnosisProbability; + if (hasilTertinggi != null) { + mainDiagnosisProbability = _getProbabilitas(hasilTertinggi); + } + // Filter out items with 100% probability and the top result List otherItems = []; @@ -626,7 +632,7 @@ class _HasilDiagnosaPageState extends State { topResultId = hasilTertinggi['id_hama']?.toString(); } - // Filter out the top result AND items with 100% probability + // Filter out the top result AND items with 100% probability AND items with higher probability than main diagnosis otherItems = itemList.where((item) { String? itemId; if (type == 'penyakit') { @@ -640,12 +646,19 @@ class _HasilDiagnosaPageState extends State { return false; } - // Skip if this item has 100% probability + // Get item probability double itemProbabilitas = _getProbabilitas(item); + + // Skip if this item has 100% probability if ((itemProbabilitas * 100).round() == 100) { return false; } + // Skip if this item has higher probability than main diagnosis + if (mainDiagnosisProbability != null && itemProbabilitas > mainDiagnosisProbability) { + return false; + } + return true; }).toList(); } else { @@ -975,7 +988,7 @@ class _HasilDiagnosaPageState extends State { ), child: Center( child: Text( - '${(value * 100).toStringAsFixed(0)}%', + '${((value * 1000).floor()/10).toStringAsFixed(1)}%', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), @@ -1055,7 +1068,7 @@ Widget _buildProbabilityIndicator(double value) { ), child: Center( child: Text( - '${(value * 100).toStringAsFixed(0)}%', + '${((value * 1000).floor()/10).toStringAsFixed(1)}%', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), diff --git a/frontend/lib/user/login_page.dart b/frontend/lib/user/login_page.dart index 7f84750..d47e68f 100644 --- a/frontend/lib/user/login_page.dart +++ b/frontend/lib/user/login_page.dart @@ -62,7 +62,7 @@ class _LoginPageState extends State { } try { - var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/auth/login"); + var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login"); var response = await http.post( url, headers: {"Content-Type": "application/json"}, diff --git a/frontend/lib/user/profile_page.dart b/frontend/lib/user/profile_page.dart index 7d44d16..4dfb1a2 100644 --- a/frontend/lib/user/profile_page.dart +++ b/frontend/lib/user/profile_page.dart @@ -63,7 +63,7 @@ class _ProfilPageState extends State { } // Buat URL untuk endpoint user API - var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/users"); + var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/users"); // Kirim permintaan GET dengan token autentikasi var response = await http.get( @@ -135,7 +135,6 @@ class _ProfilPageState extends State { _nameController.text = userData?['name'] ?? ''; _emailController.text = userData?['email'] ?? ''; _alamatController.text = userData?['alamat'] ?? ''; - _nomorTeleponController.text = userData?['nomorTelepon'] ?? ''; _passwordController.text = ''; // Empty for security showDialog( @@ -247,26 +246,26 @@ class _ProfilPageState extends State { : null, ), SizedBox(height: 16), - TextFormField( - controller: _nomorTeleponController, - decoration: InputDecoration( - labelText: 'Nomor Telepon', - prefixIcon: Icon(Icons.phone, color: Color(0xFF9DC08D)), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Color(0xFF9DC08D)), - ), - ), - keyboardType: TextInputType.phone, - validator: - (value) => - value?.isEmpty ?? true - ? 'Nomor telepon tidak boleh kosong' - : null, - ), + // TextFormField( + // controller: _nomorTeleponController, + // decoration: InputDecoration( + // labelText: 'Nomor Telepon', + // prefixIcon: Icon(Icons.phone, color: Color(0xFF9DC08D)), + // border: OutlineInputBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // focusedBorder: OutlineInputBorder( + // borderRadius: BorderRadius.circular(8), + // borderSide: BorderSide(color: Color(0xFF9DC08D)), + // ), + // ), + // keyboardType: TextInputType.phone, + // validator: + // (value) => + // value?.isEmpty ?? true + // ? 'Nomor telepon tidak boleh kosong' + // : null, + // ), ], ), ), diff --git a/frontend/lib/user/riwayat_diagnosa_page.dart b/frontend/lib/user/riwayat_diagnosa_page.dart index 69b13de..2edcb45 100644 --- a/frontend/lib/user/riwayat_diagnosa_page.dart +++ b/frontend/lib/user/riwayat_diagnosa_page.dart @@ -94,7 +94,9 @@ class _RiwayatDiagnosaPageState extends State { Future _fetchUserData() async { try { // Buat URL untuk endpoint user API - var url = Uri.parse("https://backend-sistem-pakar-diagnosa-penya.vercel.app/api/users"); + var url = Uri.parse( + "https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/users", + ); // Kirim permintaan GET dengan token autentikasi var response = await http.get( @@ -489,7 +491,7 @@ class _RiwayatDiagnosaPageState extends State { ), SizedBox(height: 4), Text( - 'Hasil: ${(riwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}', + 'Hasil: ${riwayat['hasil'] != null ? "${(((riwayat['hasil'] as num) * 1000).floor() / 10).toStringAsFixed(1)}%" : "-"}', style: TextStyle(fontSize: 14), ), SizedBox(height: 12),