From 8e92a58e4968664f91f892519172bbee93ba5378 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 13 May 2025 04:36:31 +0700 Subject: [PATCH] hasil diagnosa riwayat --- backend/.env | 12 +- backend/app.js | 2 + backend/controller/authController.js | 2 + backend/controller/historiController.js | 57 ++ backend/routes/historiRoutes.js | 97 ++++ frontend/lib/api_services/api_services.dart | 157 ++++-- frontend/lib/user/detail_riwayat_page.dart | 193 +++++++ frontend/lib/user/home_page.dart | 56 +- frontend/lib/user/login_page.dart | 4 +- frontend/lib/user/riwayat_diagnosa_page.dart | 553 +++++++++++++------ 10 files changed, 899 insertions(+), 234 deletions(-) create mode 100644 backend/controller/historiController.js create mode 100644 backend/routes/historiRoutes.js create mode 100644 frontend/lib/user/detail_riwayat_page.dart diff --git a/backend/.env b/backend/.env index d688bd8..952b683 100644 --- a/backend/.env +++ b/backend/.env @@ -4,9 +4,9 @@ DB_USER=root DB_NAME=sibayam API_URL=http://localhost:5000 JWT_SECRET=2c5t0ny38989t03cr4ny904r8xy12jc -EMAIL_HOST=sandbox.smtp.mailtrap.io -EMAIL_PORT="" -EMAIL_USER="" -EMAIL_PASS="" -SENDGRID_API_KEY="" -EMAIL_FROM="" \ No newline at end of file +EMAIL_HOST= +EMAIL_PORT= +EMAIL_USER= +EMAIL_PASS= +SENDGRID_API_KEY= +EMAIL_FROM= \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 3f23ca6..3b211f3 100644 --- a/backend/app.js +++ b/backend/app.js @@ -11,6 +11,7 @@ const penyakitRoutes = require('./routes/penyakitRoutes'); const ruleRoutes = require('./routes/ruleRoutes'); const ruleHamaRoutes = require('./routes/ruleHamaRoutes'); const diagnosaRoute = require('./routes/diagnosaRoutes'); +const historiRoutes = require('./routes/historiRoutes'); const swaggerDocs = require('./swagger'); dotenv.config(); @@ -35,6 +36,7 @@ app.use("/api/penyakit", penyakitRoutes); app.use("/api/rules_penyakit", ruleRoutes); app.use("/api/rules_hama", ruleHamaRoutes); app.use("/api/diagnosa", diagnosaRoute); +app.use("/api/histori", historiRoutes); // Swagger Documentation diff --git a/backend/controller/authController.js b/backend/controller/authController.js index b8233cc..63b1ea8 100644 --- a/backend/controller/authController.js +++ b/backend/controller/authController.js @@ -65,6 +65,8 @@ exports.login = async (req, res) => { { expiresIn: process.env.JWT_EXPIRES_IN || '1d' } ); + console.log("User ID dari backend:", user.id); + // 🔹 Kirim response dengan token dan role res.status(200).json({ message: "Login berhasil", diff --git a/backend/controller/historiController.js b/backend/controller/historiController.js new file mode 100644 index 0000000..a24a40a --- /dev/null +++ b/backend/controller/historiController.js @@ -0,0 +1,57 @@ +const { Histori, Gejala, Penyakit, Hama } = require('../models'); + +// Ambil semua histori +exports.getAllHistori = async (req, res) => { + try { + const histori = await Histori.findAll(); + + res.status(200).json({ message: 'Data Histori', data: histori }); + } catch (error) { + console.error('Error getHistori:', error); + res.status(500).json({ message: 'Terjadi kesalahan server', error: error.message }); + } + }; + +// Ambil histori berdasarkan ID user +exports.getHistoriByUserId = async (req, res) => { + const { userId } = req.params; // Ambil ID user dari parameter URL + + if (!userId || userId === 'null') { + return res.status(400).json({ message: 'User ID tidak valid' }); + } + + console.log("Menerima request untuk userId:", userId); + try { + const histori = await Histori.findAll({ + where: { userId }, // Filter berdasarkan ID user + include: [ + { + model: Gejala, + as: 'gejala', + attributes: ['id', 'kode', 'nama'] + }, + { + model: Penyakit, + as: 'penyakit', + attributes: ['id', 'nama'] + }, + { + model: Hama, + as: 'hama', + attributes: ['id', 'nama'] + } + ], + order: [['tanggal_diagnosa', 'DESC']] // Urutkan berdasarkan tanggal diagnosa terbaru + }); + + if (histori.length === 0) { + return res.status(404).json({ message: 'Tidak ada histori untuk user ini' }); + } + + res.status(200).json({ message: 'Data Histori User', data: histori }); + } catch (error) { + console.error('Error getHistoriByUserId:', error); + res.status(500).json({ message: 'Terjadi kesalahan server', error: error.message }); + } +}; + diff --git a/backend/routes/historiRoutes.js b/backend/routes/historiRoutes.js new file mode 100644 index 0000000..8b18c3a --- /dev/null +++ b/backend/routes/historiRoutes.js @@ -0,0 +1,97 @@ +const express = require('express'); +const router = express.Router(); +const { getAllHistori, getHistoriByUserId } = require('../controller/historiController'); + +/** + * @swagger + * /api/histori: + * get: + * summary: Ambil semua data histori + * tags: + * - Histori + * responses: + * 200: + * description: Berhasil mengambil data histori + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Data Histori + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * userId: + * type: integer + * example: 2 + * hasil: + * type: float + * example: 0.85 + * tanggal_diagnosa: + * type: string + * format: date-time + * example: 2025-05-12T10:00:00Z + * 500: + * description: Terjadi kesalahan server + */ +router.get('/', getAllHistori); + +/** + * @swagger + * /api/histori/user/{userId}: + * get: + * summary: Ambil data histori berdasarkan ID user + * tags: + * - Histori + * parameters: + * - in: path + * name: userId + * required: true + * description: ID user untuk mengambil histori + * schema: + * type: integer + * example: 1 + * responses: + * 200: + * description: Berhasil mengambil data histori user + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Data Histori User + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * userId: + * type: integer + * example: 1 + * hasil: + * type: float + * example: 0.85 + * tanggal_diagnosa: + * type: string + * format: date-time + * example: 2025-05-12T10:00:00Z + * 404: + * description: Tidak ada histori untuk user ini + * 500: + * description: Terjadi kesalahan server + */ +router.get('/user/:userId', getHistoriByUserId); + +module.exports = router; \ No newline at end of file diff --git a/frontend/lib/api_services/api_services.dart b/frontend/lib/api_services/api_services.dart index f6bb0e0..5576f63 100644 --- a/frontend/lib/api_services/api_services.dart +++ b/frontend/lib/api_services/api_services.dart @@ -15,37 +15,31 @@ class ApiService { static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama'; static const String userUrl = 'http://localhost:5000/api/users'; static const String diagnosaUrl = 'http://localhost:5000/api/diagnosa'; + static const String historiUrl = 'http://localhost:5000/api/histori'; static const Duration timeout = Duration(seconds: 15); /// Fungsi untuk mengirim gejala dan menerima hasil diagnosa -// Kirim gejala dan dapatkan hasil diagnosa - Future> diagnosa(List gejalIds) async { - // Konversi string ID menjadi integer jika backend Anda membutuhkan integer - List gejalaNumerik = []; - try { - gejalaNumerik = gejalIds.map((id) => int.parse(id)).toList(); - } catch (e) { - print("Error saat konversi ID gejala ke integer: $e"); - // Jika konversi gagal, gunakan ID string saja - final response = await http.post( - Uri.parse('$diagnosaUrl'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'gejala': gejalIds}), - ); +Future> diagnosa(List gejalaIds) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Gagal melakukan diagnosa: ${response.statusCode} - ${response.body}'); - } + List parsedGejala; + try { + // Coba konversi ke integer jika bisa + parsedGejala = gejalaIds.map((id) => int.parse(id)).toList(); + } catch (e) { + print("Konversi ke integer gagal, gunakan string ID."); + parsedGejala = gejalaIds; } - // Jika konversi berhasil, gunakan ID numerik final response = await http.post( - Uri.parse('$diagnosaUrl'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'gejala': gejalaNumerik}), - ); + Uri.parse(diagnosaUrl), + headers: { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }, + body: json.encode({'gejala': parsedGejala}), + ).timeout(timeout); if (response.statusCode == 200) { return json.decode(response.body); @@ -54,32 +48,108 @@ class ApiService { } } +Future>> getHistoriDiagnosa(String userId) async { + try { + // Ambil token dari SharedPreferences + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null || token.isEmpty) { + throw Exception("Token tidak valid"); + } + + final url = '$historiUrl/user/$userId'; + print("Fetching histori from URL: $url"); + + final response = await http.get( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + ); + + print("Response Status Code: ${response.statusCode}"); + print("Response Body: ${response.body}"); + + if (response.statusCode == 200) { + final Map responseData = json.decode(response.body); + + if (responseData.containsKey('data') && responseData['data'] is List) { + return List>.from(responseData['data']); + } else { + throw Exception('Format respons tidak valid.'); + } + } else { + throw Exception('Gagal memuat histori: ${response.statusCode}'); + } + } catch (e) { + print("Error fetching data: $e"); + throw Exception('Terjadi kesalahan saat mengambil histori: $e'); + } +} + +Future>> fetchHistoriDenganDetail(String userId) async { + try { + // Panggil API untuk mendapatkan data histori + final historiResponse = await getHistoriDiagnosa(userId); + + // Proses data histori + List> 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'], + "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: $result"); + return result; + } catch (e) { + print("Error fetching histori dengan detail: $e"); + return []; + } +} + // Fungsi Login (dengan perbaikan) static Future> loginUser( - String email, - String password, - ) async { - try { - final response = await http.post( - Uri.parse("$baseUrl/login"), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({'email': email, 'password': password}), - ); + String email, + String password, +) async { + try { + final response = await http.post( + Uri.parse("$baseUrl/login"), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({'email': email, 'password': password}), + ); - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); + print("Response Status: ${response.statusCode}"); + print("Response Body: ${response.body}"); - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else { - throw Exception("Login gagal: ${response.body}"); - } - } catch (e) { - print("Error: $e"); - throw Exception("Terjadi kesalahan saat login"); + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + + // Simpan userId ke SharedPreferences + final SharedPreferences prefs = await SharedPreferences.getInstance(); + print("User ID dari respons login: ${responseData['userId']}"); // Tambahkan log + await prefs.setString('userId', responseData['userId'].toString()); + + return responseData; + } else { + throw Exception("Login gagal: ${response.body}"); } + } catch (e) { + print("Error: $e"); + throw Exception("Terjadi kesalahan saat login"); } +} // Fungsi Logout static Future logoutUser() async { @@ -283,7 +353,6 @@ class ApiService { } } - // Tambah hama baru (kode otomatis) Future> createHama( String nama, diff --git a/frontend/lib/user/detail_riwayat_page.dart b/frontend/lib/user/detail_riwayat_page.dart new file mode 100644 index 0000000..a6670f5 --- /dev/null +++ b/frontend/lib/user/detail_riwayat_page.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/api_services/api_services.dart'; +import 'dart:typed_data'; + +class DetailRiwayatPage extends StatelessWidget { + final Map detailRiwayat; + final ApiService apiService = ApiService(); + + DetailRiwayatPage({required this.detailRiwayat}) { + print("Detail Riwayat Data: $detailRiwayat"); + } + + Future> fetchDetailData() async { + final diagnosisType = detailRiwayat['diagnosis_type']; + final diagnosisName = detailRiwayat['diagnosis']; + + print("Diagnosis Type: $diagnosisType, Name: $diagnosisName"); + + try { + if (diagnosisType == 'penyakit') { + // Dapatkan daftar semua penyakit + final penyakitList = await apiService.getPenyakit(); + // Cari penyakit berdasarkan nama + final penyakit = penyakitList.firstWhere( + (p) => + p['nama'].toString().toLowerCase() == + diagnosisName.toString().toLowerCase(), + orElse: () => throw Exception('Penyakit tidak ditemukan'), + ); + + final id = penyakit['id']; + print("Found Penyakit ID: $id"); + + // Ambil gambar jika ID ditemukan + final imageBytes = + id != null ? await apiService.getPenyakitImageBytes(id) : null; + + return {...penyakit, 'imageBytes': imageBytes}; + } else if (diagnosisType == 'hama') { + // Dapatkan daftar semua hama + final hamaList = await apiService.getHama(); + // Cari hama berdasarkan nama + final hama = hamaList.firstWhere( + (h) => + h['nama'].toString().toLowerCase() == + diagnosisName.toString().toLowerCase(), + orElse: () => throw Exception('Hama tidak ditemukan'), + ); + + final id = hama['id']; + print("Found Hama ID: $id"); + + // Ambil gambar jika ID ditemukan + final imageBytes = + id != null ? await apiService.getHamaImageBytes(id) : null; + + return {...hama, 'imageBytes': imageBytes}; + } else { + throw Exception('Tipe diagnosis tidak valid'); + } + } catch (e) { + print("Error in fetchDetailData: $e"); + throw Exception("Gagal mengambil detail: $e"); + } + } + + @override + Widget build(BuildContext context) { + final gejalaList = (detailRiwayat['gejala'] as List).join(', '); + + return Scaffold( + appBar: AppBar( + backgroundColor: Color(0xFF9DC08D), + title: Text( + 'Detail Riwayat Diagnosa', + style: TextStyle(color: Colors.white), + ), + ), + body: FutureBuilder>( + future: fetchDetailData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text("Error: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data == null) { + return Center(child: Text("Data tidak ditemukan")); + } + + final detailData = snapshot.data!; + final imageBytes = detailData['imageBytes'] as Uint8List?; + final deskripsi = + detailData['deskripsi'] ?? 'Deskripsi tidak tersedia'; + final penanganan = + detailData['penanganan'] ?? 'Penanganan tidak tersedia'; + + return SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Diagnosis: ${detailRiwayat['diagnosis']}', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 12), + if (imageBytes != null) + // Ganti bagian Container untuk gambar dengan kode berikut + if (imageBytes != null) + Container( + width: double.infinity, + constraints: BoxConstraints( + maxHeight: 250, // Tinggi maksimal yang konsisten + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: + Colors + .grey[200], // Warna background untuk area gambar + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Center( + // Menambahkan Center widget + child: AspectRatio( + aspectRatio: + 16 / 9, // Rasio aspek yang konsisten + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage(imageBytes), + fit: + BoxFit + .contain, // Menggunakan contain untuk menjaga aspek rasio + ), + ), + ), + ), + ), + ), + ), + SizedBox(height: 12), + Text('Gejala: $gejalaList', style: TextStyle(fontSize: 16)), + SizedBox(height: 8), + Text( + 'Hasil: ${(detailRiwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + 'Tanggal: ${detailRiwayat['tanggal_diagnosa'] ?? "-"}', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 16), + Text( + 'Deskripsi:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text(deskripsi, style: TextStyle(fontSize: 16)), + SizedBox(height: 16), + Text( + 'Penanganan:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text(penanganan, style: TextStyle(fontSize: 16)), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/frontend/lib/user/home_page.dart b/frontend/lib/user/home_page.dart index 5ebce63..e647f01 100644 --- a/frontend/lib/user/home_page.dart +++ b/frontend/lib/user/home_page.dart @@ -4,10 +4,36 @@ import 'diagnosa_page.dart'; import 'riwayat_diagnosa_page.dart'; import 'profile_page.dart'; import 'basis_pengetahuan_page.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} +class _HomePageState extends State { + String userId = ''; // Variabel untuk menyimpan userId + + @override + void initState() { + super.initState(); + } + + Future navigateToRiwayatDiagnosaPage(BuildContext context) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + print("Navigating to RiwayatDiagnosaPage with userId: $userId"); + + if (userId.isEmpty) { + print("Error: User ID tidak ditemukan di SharedPreferences"); + // Tampilkan pesan error atau arahkan ke halaman login + return; + } + } + + @override Widget build(BuildContext context) { return Scaffold( @@ -89,21 +115,25 @@ class HomePage extends StatelessWidget { height: 48, ), onTap: () { + navigateToRiwayatDiagnosaPage(context); Navigator.push( context, MaterialPageRoute( - builder: (context) => RiwayatDiagnosaPage(), - ), // Perbaikan di sini + builder: + (context) => RiwayatDiagnosaPage( + userId: userId, + ), // Kirimkan userId sebagai String + ), ); }, ), ButtonMenu( title: "Profile", customIcon: Image.asset( - 'assets/images/Test Account.png', - width: 48, - height: 48, - ), + 'assets/images/Test Account.png', + width: 48, + height: 48, + ), onTap: () { Navigator.push( context, @@ -122,10 +152,10 @@ class HomePage extends StatelessWidget { ButtonMenu( title: "Basis Pengetahuan", customIcon: Image.asset( - 'assets/images/Literature.png', - width: 48, - height: 48, - ), + 'assets/images/Literature.png', + width: 48, + height: 48, + ), onTap: () { Navigator.push( context, @@ -194,11 +224,11 @@ class ButtonMenu extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - customIcon, + customIcon, const SizedBox(height: 5), Text( title, - textAlign: TextAlign.center, + textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], diff --git a/frontend/lib/user/login_page.dart b/frontend/lib/user/login_page.dart index 35e0b62..fd9b8ac 100644 --- a/frontend/lib/user/login_page.dart +++ b/frontend/lib/user/login_page.dart @@ -51,10 +51,12 @@ class _LoginPageState extends State { if (response.statusCode == 200) { // Simpan token & role ke SharedPreferences - SharedPreferences prefs = await SharedPreferences.getInstance(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + print("User ID dari respons login: ${responseData['userId']}"); await prefs.setString('token', responseData['token']); await prefs.setString('role', responseData['role']); await prefs.setString('email', email); + await prefs.setString('userId', responseData['userId'].toString()); // Redirect berdasarkan role if (responseData['role'] == 'admin') { diff --git a/frontend/lib/user/riwayat_diagnosa_page.dart b/frontend/lib/user/riwayat_diagnosa_page.dart index e636722..288de2c 100644 --- a/frontend/lib/user/riwayat_diagnosa_page.dart +++ b/frontend/lib/user/riwayat_diagnosa_page.dart @@ -1,37 +1,292 @@ import 'package:flutter/material.dart'; +import 'package:frontend/api_services/api_services.dart'; +import 'detail_riwayat_page.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'login_page.dart'; -class RiwayatDiagnosaPage extends StatelessWidget { - final List> diagnosaList = [ - { - "nama": "Karat Putih", - "deskripsi": "Penyakit yang umum pada bayam.", - "penyakit": "Karat Putih", - "hama": "Tidak ada hama spesifik", - "penanganan": "Gunakan fungisida sesuai anjuran dan potong daun yang terinfeksi.", - "gambar": "assets/images/karat putih.jpeg", - }, - { - "nama": "Virus Keriting", - "deskripsi": "Disebabkan oleh infeksi virus.", - "penyakit": "Virus Keriting", - "hama": "Tidak ada hama spesifik", - "penanganan": "Musnahkan tanaman terinfeksi dan kontrol vektor seperti kutu daun." - }, - { - "nama": "Kekurangan Mangan", - "deskripsi": "Kekurangan unsur hara mikro.", - "penyakit": "Kekurangan Mangan", - "hama": "Tidak ada hama spesifik", - "penanganan": "Tambahkan pupuk yang mengandung mangan (Mn)." - }, - { - "nama": "Downy Mildew", - "deskripsi": "Penyakit jamur pada bayam.", - "penyakit": "Downy Mildew", - "hama": "Tidak ada hama spesifik", - "penanganan": "Gunakan fungisida berbahan aktif metalaxyl dan perbaiki drainase tanah." - }, - ]; +class RiwayatDiagnosaPage extends StatefulWidget { + final String? userId; + + RiwayatDiagnosaPage({this.userId}); + + @override + _RiwayatDiagnosaPageState createState() => _RiwayatDiagnosaPageState(); +} + +class _RiwayatDiagnosaPageState extends State { + List> _riwayatData = []; + final ApiService apiService = ApiService(); + bool _isLoading = true; + String? _errorMessage; + String? _userId; + String? _token; + String? _email; + + @override + void initState() { + super.initState(); + _loadUserDataAndFetchHistori(); + } + + Future _loadUserDataAndFetchHistori() async { + try { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + // Ambil data user yang sedang login dari SharedPreferences + SharedPreferences prefs = await SharedPreferences.getInstance(); + _token = prefs.getString('token'); + _email = prefs.getString('email'); + + // Check if already has userId from widget constructor + _userId = widget.userId; + + print("Token from SharedPreferences: $_token"); + print("Email from SharedPreferences: $_email"); + print("Initial userId: $_userId"); + + // If no token or email, we can't proceed + if (_token == null || _email == null) { + throw Exception('Sesi login tidak ditemukan, silahkan login kembali'); + } + + // If we don't have userId yet, we need to fetch user data first + if (_userId == null || _userId!.isEmpty) { + await _fetchUserData(); + } + + // Double-check if userId is still null after fetching + if (_userId == null || _userId!.isEmpty) { + throw Exception('Gagal mendapatkan ID pengguna'); + } + + // Now that we have userId, fetch the history data + await _fetchHistoriData(); + } catch (e) { + print("Error in _loadUserDataAndFetchHistori: $e"); + + // Check if the error is about authentication + if (e.toString().contains('login') || e.toString().contains('sesi')) { + // Clear any existing user data and redirect to login + await ApiService.logoutUser(); + + // Navigate to login page in the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => LoginPage()), + ); + }); + } + + setState(() { + _isLoading = false; + _errorMessage = e.toString(); + }); + } + } + + // Fetch user data to get user ID + Future _fetchUserData() async { + try { + // Buat URL untuk endpoint user API + var url = Uri.parse("http://localhost:5000/api/users"); + + // Kirim permintaan GET dengan token autentikasi + var response = await http.get( + url, + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer $_token", + }, + ); + + if (response.statusCode == 200) { + // Parse data respons + List users = jsonDecode(response.body); + print("Email login: $_email"); + print("Users data from server: ${users.length} users"); + + // Cari user dengan email yang sama dengan yang login + Map? currentUser; + for (var user in users) { + if (user['email'].toString().toLowerCase() == _email!.toLowerCase()) { + currentUser = Map.from(user); + print( + "User found: ${currentUser['name']} with ID: ${currentUser['id']}", + ); + break; + } + } + + if (currentUser == null) { + throw Exception('Data pengguna tidak ditemukan'); + } + + // Save userId to SharedPreferences for future use + final prefs = await SharedPreferences.getInstance(); + prefs.setString('userId', currentUser['id'].toString()); + + // Update state with user ID + setState(() { + _userId = currentUser!['id'].toString(); + }); + + print("User ID set to: $_userId"); + } else if (response.statusCode == 401) { + // Token tidak valid atau expired + await ApiService.logoutUser(); + throw Exception('Sesi habis, silahkan login kembali'); + } else { + throw Exception('Gagal mengambil data: ${response.statusCode}'); + } + } catch (e) { + print("Error fetching user data: $e"); + throw e; // Re-throw for the caller to handle + } + } + + List> _groupHistoriByDiagnosis( + List> data, +) { + final Map> groupedData = {}; + print("Data mentah dari API: $data"); + + for (var item in data) { + final String? penyakitNama = item['penyakit_nama']; + final String? hamaNama = item['hama_nama']; + + final int? idPenyakit = item['id_penyakit']; + final int? idHama = item['id_hama']; + + final hasPenyakit = penyakitNama != null && penyakitNama.toString().isNotEmpty; + final hasHama = hamaNama != null && hamaNama.toString().isNotEmpty; + + if (!hasPenyakit && !hasHama) { + print("Item dilewati karena tidak memiliki penyakit atau hama: $item"); + continue; + } + + // Gabungkan nama penyakit dan hama jika keduanya ada + String diagnosisKey = ''; + if (hasPenyakit && hasHama) { + diagnosisKey = '$penyakitNama & $hamaNama'; + } else if (hasPenyakit) { + diagnosisKey = penyakitNama!; + } else { + diagnosisKey = hamaNama!; + } + + // Tentukan diagnosis_type hanya sebagai referensi + String diagnosisType = + hasPenyakit && hasHama + ? 'penyakit & hama' + : hasPenyakit + ? 'penyakit' + : 'hama'; + + // Inisialisasi grup jika belum ada + if (!groupedData.containsKey(diagnosisKey)) { + groupedData[diagnosisKey] = { + 'diagnosis': diagnosisKey, + 'diagnosis_type': diagnosisType, + 'gejala': [], + 'hasil': item['hasil'], + 'tanggal_diagnosa': item['tanggal_diagnosa'], + 'id_penyakit': idPenyakit, + 'id_hama': idHama, + }; + } + + // Tambahkan gejala jika belum ada + if (item['gejala_nama'] != null) { + final gejalaNama = item['gejala_nama']; + if (!groupedData[diagnosisKey]!['gejala'].contains(gejalaNama)) { + groupedData[diagnosisKey]!['gejala'].add(gejalaNama); + } + } + } + + final result = groupedData.values.toList(); + print("Hasil pengelompokan: $result"); + return result; +} + + + Future _fetchHistoriData() async { + try { + print("Fetching histori dengan userId: $_userId"); + + // Panggil API untuk mendapatkan data histori + final historiResponse = await apiService.fetchHistoriDenganDetail( + _userId!, + ); + + // Kelompokkan data berdasarkan diagnosis + final groupedData = _groupHistoriByDiagnosis(historiResponse); + + setState(() { + _riwayatData = + groupedData; // Use groupedData instead of historiResponse + _isLoading = false; + }); + + print("Successfully fetched ${_riwayatData.length} history records"); + } catch (e) { + print("Error fetching histori data: $e"); + setState(() { + _isLoading = false; + _errorMessage = "Gagal memuat data riwayat: ${e.toString()}"; + }); + } + } + + Widget _buildErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 60, color: Colors.white70), + SizedBox(height: 16), + Text( + _errorMessage ?? 'Terjadi kesalahan', + style: TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + ), + SizedBox(height: 24), + ElevatedButton( + onPressed: _loadUserDataAndFetchHistori, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Color(0xFF9DC08D), + ), + child: Text('Coba Lagi'), + ), + ], + ), + ); + } + + Widget _buildEmptyHistoryWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.history, size: 60, color: Colors.white70), + SizedBox(height: 16), + Text( + 'Belum ada riwayat diagnosa.', + style: TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + ), + ], + ), + ); + } @override Widget build(BuildContext context) { @@ -39,154 +294,112 @@ class RiwayatDiagnosaPage extends StatelessWidget { backgroundColor: Color(0xFF9DC08D), appBar: AppBar( backgroundColor: Color(0xFF9DC08D), - title: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(right: 30), // Geser ke kiri - child: Text( - "Riwayat Diagnosa", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), - ), + title: Text( + "Riwayat Diagnosa", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), leading: IconButton( icon: Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.pop(context), ), ), body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - SizedBox(height: 16), - Expanded( - child: ListView.builder( - itemCount: diagnosaList.length, - itemBuilder: (context, index) { - final diagnosa = diagnosaList[index]; - return Card( - elevation: 4, - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - title: Text( - diagnosa["nama"] ?? "Tidak ada data", - style: TextStyle(fontWeight: FontWeight.bold), + padding: const EdgeInsets.all(16.0), + child: + _isLoading + ? Center(child: CircularProgressIndicator(color: Colors.white)) + : _errorMessage != null + ? _buildErrorWidget() + : _riwayatData.isEmpty + ? _buildEmptyHistoryWidget() + : ListView.builder( + itemCount: _riwayatData.length, + itemBuilder: (context, index) { + final riwayat = _riwayatData[index]; + + // Safely handle potential null values + List gejalaList = []; + if (riwayat.containsKey('gejala') && + riwayat['gejala'] != null) { + gejalaList = riwayat['gejala'] as List; + } + + final gejalaText = + gejalaList.isEmpty + ? "Tidak ada gejala tercatat" + : gejalaList.join(', '); + + return Card( + margin: EdgeInsets.only(bottom: 12.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - subtitle: Text(diagnosa["deskripsi"] ?? "Deskripsi tidak tersedia"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DetailRiwayatPage( - detailRiwayat: { - "penyakit": diagnosa["penyakit"] ?? "", - "hama": diagnosa["hama"] ?? "", - "penanganan": diagnosa["penanganan"] ?? "", - "gambar": diagnosa["gambar"] ?? "", - }, + elevation: 3, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Diagnosis: ${riwayat['diagnosis']}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - ), - ); - }, - ), - ); - }, - ), - ), - ], - ), + SizedBox(height: 8), + Text( + 'Gejala: $gejalaText', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 4), + Text( + 'Hasil: ${(riwayat['hasil'] as num?)?.toStringAsFixed(2) ?? "-"}', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 8), + Text( + 'Tanggal: ${riwayat['tanggal_diagnosa'] ?? "-"}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + print("Navigating to DetailRiwayatPage with data: $riwayat"); + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => DetailRiwayatPage( + detailRiwayat: + riwayat, // Kirim data riwayat ke halaman detail + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + foregroundColor: Colors.white, + ), + child: Text('Lihat Detail'), + ), + ), + ], + ), + ), + ); + }, + ), ), ); } } - -class DetailRiwayatPage extends StatelessWidget { - final Map detailRiwayat; - - DetailRiwayatPage({required this.detailRiwayat}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color(0xFF9DC08D), - appBar: AppBar( - backgroundColor: Color(0xFF9DC08D), - title: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(right: 30), - child: Text( - "Hasil Diagnosa", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), - ), - ), - ), - leading: IconButton( - icon: Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - if (detailRiwayat['gambar'] != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.asset( - detailRiwayat['gambar']!, - height: 200, - width: 200, - fit: BoxFit.cover, - ), - ), - SizedBox(height: 16), - Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Nama Penyakit: ${detailRiwayat['penyakit']}", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black), - ), - SizedBox(height: 16), - Text( - "Nama Hama: ${detailRiwayat['hama']}", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black), - ), - ], - ), - ), - ), - SizedBox(height: 16), - Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Cara Penanganan:", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black), - ), - SizedBox(height: 10), - Text( - detailRiwayat['penanganan'] ?? "Data tidak tersedia", - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file