hasil diagnosa riwayat

This commit is contained in:
unknown 2025-05-13 04:36:31 +07:00
parent 3a4c8bfeb0
commit 8e92a58e49
10 changed files with 899 additions and 234 deletions

View File

@ -4,9 +4,9 @@ DB_USER=root
DB_NAME=sibayam DB_NAME=sibayam
API_URL=http://localhost:5000 API_URL=http://localhost:5000
JWT_SECRET=2c5t0ny38989t03cr4ny904r8xy12jc JWT_SECRET=2c5t0ny38989t03cr4ny904r8xy12jc
EMAIL_HOST=sandbox.smtp.mailtrap.io EMAIL_HOST=
EMAIL_PORT="" EMAIL_PORT=
EMAIL_USER="" EMAIL_USER=
EMAIL_PASS="" EMAIL_PASS=
SENDGRID_API_KEY="" SENDGRID_API_KEY=
EMAIL_FROM="" EMAIL_FROM=

View File

@ -11,6 +11,7 @@ const penyakitRoutes = require('./routes/penyakitRoutes');
const ruleRoutes = require('./routes/ruleRoutes'); const ruleRoutes = require('./routes/ruleRoutes');
const ruleHamaRoutes = require('./routes/ruleHamaRoutes'); const ruleHamaRoutes = require('./routes/ruleHamaRoutes');
const diagnosaRoute = require('./routes/diagnosaRoutes'); const diagnosaRoute = require('./routes/diagnosaRoutes');
const historiRoutes = require('./routes/historiRoutes');
const swaggerDocs = require('./swagger'); const swaggerDocs = require('./swagger');
dotenv.config(); dotenv.config();
@ -35,6 +36,7 @@ app.use("/api/penyakit", penyakitRoutes);
app.use("/api/rules_penyakit", ruleRoutes); app.use("/api/rules_penyakit", ruleRoutes);
app.use("/api/rules_hama", ruleHamaRoutes); app.use("/api/rules_hama", ruleHamaRoutes);
app.use("/api/diagnosa", diagnosaRoute); app.use("/api/diagnosa", diagnosaRoute);
app.use("/api/histori", historiRoutes);
// Swagger Documentation // Swagger Documentation

View File

@ -65,6 +65,8 @@ exports.login = async (req, res) => {
{ expiresIn: process.env.JWT_EXPIRES_IN || '1d' } { expiresIn: process.env.JWT_EXPIRES_IN || '1d' }
); );
console.log("User ID dari backend:", user.id);
// 🔹 Kirim response dengan token dan role // 🔹 Kirim response dengan token dan role
res.status(200).json({ res.status(200).json({
message: "Login berhasil", message: "Login berhasil",

View File

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

View File

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

View File

@ -15,37 +15,31 @@ class ApiService {
static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama'; static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama';
static const String userUrl = 'http://localhost:5000/api/users'; static const String userUrl = 'http://localhost:5000/api/users';
static const String diagnosaUrl = 'http://localhost:5000/api/diagnosa'; 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); static const Duration timeout = Duration(seconds: 15);
/// Fungsi untuk mengirim gejala dan menerima hasil diagnosa /// Fungsi untuk mengirim gejala dan menerima hasil diagnosa
// Kirim gejala dan dapatkan hasil diagnosa Future<Map<String, dynamic>> diagnosa(List<String> gejalaIds) async {
Future<Map<String, dynamic>> diagnosa(List<String> gejalIds) async { final SharedPreferences prefs = await SharedPreferences.getInstance();
// Konversi string ID menjadi integer jika backend Anda membutuhkan integer final token = prefs.getString('token');
List<int> 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}),
);
if (response.statusCode == 200) { List<dynamic> parsedGejala;
return json.decode(response.body); try {
} else { // Coba konversi ke integer jika bisa
throw Exception('Gagal melakukan diagnosa: ${response.statusCode} - ${response.body}'); 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( final response = await http.post(
Uri.parse('$diagnosaUrl'), Uri.parse(diagnosaUrl),
headers: {'Content-Type': 'application/json'}, headers: {
body: json.encode({'gejala': gejalaNumerik}), 'Content-Type': 'application/json',
); if (token != null) 'Authorization': 'Bearer $token',
},
body: json.encode({'gejala': parsedGejala}),
).timeout(timeout);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return json.decode(response.body); return json.decode(response.body);
@ -54,32 +48,108 @@ class ApiService {
} }
} }
Future<List<Map<String, dynamic>>> 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<String, dynamic> responseData = json.decode(response.body);
if (responseData.containsKey('data') && responseData['data'] is List) {
return List<Map<String, dynamic>>.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<List<Map<String, dynamic>>> fetchHistoriDenganDetail(String userId) async {
try {
// Panggil API untuk mendapatkan data histori
final historiResponse = await getHistoriDiagnosa(userId);
// 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'],
"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) // Fungsi Login (dengan perbaikan)
static Future<Map<String, dynamic>> loginUser( static Future<Map<String, dynamic>> loginUser(
String email, String email,
String password, String password,
) async { ) async {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse("$baseUrl/login"), Uri.parse("$baseUrl/login"),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({'email': email, 'password': password}), body: jsonEncode({'email': email, 'password': password}),
); );
print("Response Status: ${response.statusCode}"); print("Response Status: ${response.statusCode}");
print("Response Body: ${response.body}"); print("Response Body: ${response.body}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
return jsonDecode(response.body); final responseData = jsonDecode(response.body);
} else {
throw Exception("Login gagal: ${response.body}"); // Simpan userId ke SharedPreferences
} final SharedPreferences prefs = await SharedPreferences.getInstance();
} catch (e) { print("User ID dari respons login: ${responseData['userId']}"); // Tambahkan log
print("Error: $e"); await prefs.setString('userId', responseData['userId'].toString());
throw Exception("Terjadi kesalahan saat login");
return responseData;
} else {
throw Exception("Login gagal: ${response.body}");
} }
} catch (e) {
print("Error: $e");
throw Exception("Terjadi kesalahan saat login");
} }
}
// Fungsi Logout // Fungsi Logout
static Future<void> logoutUser() async { static Future<void> logoutUser() async {
@ -283,7 +353,6 @@ class ApiService {
} }
} }
// Tambah hama baru (kode otomatis) // Tambah hama baru (kode otomatis)
Future<Map<String, dynamic>> createHama( Future<Map<String, dynamic>> createHama(
String nama, String nama,

View File

@ -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<String, dynamic> detailRiwayat;
final ApiService apiService = ApiService();
DetailRiwayatPage({required this.detailRiwayat}) {
print("Detail Riwayat Data: $detailRiwayat");
}
Future<Map<String, dynamic>> 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<Map<String, dynamic>>(
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)),
],
),
),
),
);
},
),
);
}
}

View File

@ -4,9 +4,35 @@ import 'diagnosa_page.dart';
import 'riwayat_diagnosa_page.dart'; import 'riwayat_diagnosa_page.dart';
import 'profile_page.dart'; import 'profile_page.dart';
import 'basis_pengetahuan_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<HomePage> {
String userId = ''; // Variabel untuk menyimpan userId
@override
void initState() {
super.initState();
}
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -89,21 +115,25 @@ class HomePage extends StatelessWidget {
height: 48, height: 48,
), ),
onTap: () { onTap: () {
navigateToRiwayatDiagnosaPage(context);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => RiwayatDiagnosaPage(), builder:
), // Perbaikan di sini (context) => RiwayatDiagnosaPage(
userId: userId,
), // Kirimkan userId sebagai String
),
); );
}, },
), ),
ButtonMenu( ButtonMenu(
title: "Profile", title: "Profile",
customIcon: Image.asset( customIcon: Image.asset(
'assets/images/Test Account.png', 'assets/images/Test Account.png',
width: 48, width: 48,
height: 48, height: 48,
), ),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@ -122,10 +152,10 @@ class HomePage extends StatelessWidget {
ButtonMenu( ButtonMenu(
title: "Basis Pengetahuan", title: "Basis Pengetahuan",
customIcon: Image.asset( customIcon: Image.asset(
'assets/images/Literature.png', 'assets/images/Literature.png',
width: 48, width: 48,
height: 48, height: 48,
), ),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,

View File

@ -51,10 +51,12 @@ class _LoginPageState extends State<LoginPage> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
// Simpan token & role ke SharedPreferences // 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('token', responseData['token']);
await prefs.setString('role', responseData['role']); await prefs.setString('role', responseData['role']);
await prefs.setString('email', email); await prefs.setString('email', email);
await prefs.setString('userId', responseData['userId'].toString());
// Redirect berdasarkan role // Redirect berdasarkan role
if (responseData['role'] == 'admin') { if (responseData['role'] == 'admin') {

View File

@ -1,37 +1,292 @@
import 'package:flutter/material.dart'; 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 { class RiwayatDiagnosaPage extends StatefulWidget {
final List<Map<String, String>> diagnosaList = [ final String? userId;
{
"nama": "Karat Putih", RiwayatDiagnosaPage({this.userId});
"deskripsi": "Penyakit yang umum pada bayam.",
"penyakit": "Karat Putih", @override
"hama": "Tidak ada hama spesifik", _RiwayatDiagnosaPageState createState() => _RiwayatDiagnosaPageState();
"penanganan": "Gunakan fungisida sesuai anjuran dan potong daun yang terinfeksi.", }
"gambar": "assets/images/karat putih.jpeg",
}, class _RiwayatDiagnosaPageState extends State<RiwayatDiagnosaPage> {
{ List<Map<String, dynamic>> _riwayatData = [];
"nama": "Virus Keriting", final ApiService apiService = ApiService();
"deskripsi": "Disebabkan oleh infeksi virus.", bool _isLoading = true;
"penyakit": "Virus Keriting", String? _errorMessage;
"hama": "Tidak ada hama spesifik", String? _userId;
"penanganan": "Musnahkan tanaman terinfeksi dan kontrol vektor seperti kutu daun." String? _token;
}, String? _email;
{
"nama": "Kekurangan Mangan", @override
"deskripsi": "Kekurangan unsur hara mikro.", void initState() {
"penyakit": "Kekurangan Mangan", super.initState();
"hama": "Tidak ada hama spesifik", _loadUserDataAndFetchHistori();
"penanganan": "Tambahkan pupuk yang mengandung mangan (Mn)." }
},
{ Future<void> _loadUserDataAndFetchHistori() async {
"nama": "Downy Mildew", try {
"deskripsi": "Penyakit jamur pada bayam.", setState(() {
"penyakit": "Downy Mildew", _isLoading = true;
"hama": "Tidak ada hama spesifik", _errorMessage = null;
"penanganan": "Gunakan fungisida berbahan aktif metalaxyl dan perbaiki drainase tanah." });
},
]; // 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<void> _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<dynamic> 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<String, dynamic>? currentUser;
for (var user in users) {
if (user['email'].toString().toLowerCase() == _email!.toLowerCase()) {
currentUser = Map<String, dynamic>.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<Map<String, dynamic>> _groupHistoriByDiagnosis(
List<Map<String, dynamic>> data,
) {
final Map<String, Map<String, dynamic>> 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': <String>[],
'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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -39,153 +294,111 @@ class RiwayatDiagnosaPage extends StatelessWidget {
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
appBar: AppBar( appBar: AppBar(
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
title: Align( title: Text(
alignment: Alignment.topCenter, "Riwayat Diagnosa",
child: Padding( style: TextStyle(
padding: const EdgeInsets.only(right: 30), // Geser ke kiri fontSize: 24,
child: Text( fontWeight: FontWeight.bold,
"Riwayat Diagnosa", color: Colors.white,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
),
), ),
), ),
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white), icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.pop(context),
), ),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.all(16.0),
child: Column( child:
children: [ _isLoading
SizedBox(height: 16), ? Center(child: CircularProgressIndicator(color: Colors.white))
Expanded( : _errorMessage != null
child: ListView.builder( ? _buildErrorWidget()
itemCount: diagnosaList.length, : _riwayatData.isEmpty
itemBuilder: (context, index) { ? _buildEmptyHistoryWidget()
final diagnosa = diagnosaList[index]; : ListView.builder(
return Card( itemCount: _riwayatData.length,
elevation: 4, itemBuilder: (context, index) {
margin: const EdgeInsets.symmetric(vertical: 8), final riwayat = _riwayatData[index];
child: ListTile(
title: Text( // Safely handle potential null values
diagnosa["nama"] ?? "Tidak ada data", List<dynamic> gejalaList = [];
style: TextStyle(fontWeight: FontWeight.bold), if (riwayat.containsKey('gejala') &&
riwayat['gejala'] != null) {
gejalaList = riwayat['gejala'] as List<dynamic>;
}
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"), elevation: 3,
onTap: () { child: Padding(
Navigator.push( padding: const EdgeInsets.all(12.0),
context, child: Column(
MaterialPageRoute( crossAxisAlignment: CrossAxisAlignment.start,
builder: (context) => DetailRiwayatPage( children: [
detailRiwayat: { Text(
"penyakit": diagnosa["penyakit"] ?? "", 'Diagnosis: ${riwayat['diagnosis']}',
"hama": diagnosa["hama"] ?? "", style: TextStyle(
"penanganan": diagnosa["penanganan"] ?? "", fontSize: 18,
"gambar": diagnosa["gambar"] ?? "", 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,
class DetailRiwayatPage extends StatelessWidget { color: Colors.grey[600],
final Map<String, String> detailRiwayat; ),
),
DetailRiwayatPage({required this.detailRiwayat}); SizedBox(height: 12),
Align(
@override alignment: Alignment.centerRight,
Widget build(BuildContext context) { child: ElevatedButton(
return Scaffold( onPressed: () {
backgroundColor: Color(0xFF9DC08D), print("Navigating to DetailRiwayatPage with data: $riwayat");
appBar: AppBar( Navigator.push(
backgroundColor: Color(0xFF9DC08D), context,
title: Align( MaterialPageRoute(
alignment: Alignment.topCenter, builder:
child: Padding( (context) => DetailRiwayatPage(
padding: const EdgeInsets.only(right: 30), detailRiwayat:
child: Text( riwayat, // Kirim data riwayat ke halaman detail
"Hasil Diagnosa", ),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), ),
), );
), },
), style: ElevatedButton.styleFrom(
leading: IconButton( backgroundColor: Color(0xFF9DC08D),
icon: Icon(Icons.arrow_back, color: Colors.white), foregroundColor: Colors.white,
onPressed: () => Navigator.of(context).pop(), ),
), child: Text('Lihat Detail'),
), ),
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),
),
],
),
),
),
],
),
), ),
); );
} }