TIF_NGANJUK_E41211992/lib/pages/result_page.dart

1435 lines
54 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../data/disease_data.dart';
import '../models/disease.dart';
import '../models/symptom.dart';
class DiseaseDetailPage extends StatelessWidget {
final Disease disease;
final Map<String, dynamic> result;
const DiseaseDetailPage({
Key? key,
required this.disease,
required this.result,
}) : super(key: key);
static const Map<String, String> diseaseImageMap = {
'P1': 'serkospora.jpg',
'P2': 'Alternaria.PNG',
'P3': 'Fitoftora.jpg',
'P4': 'antraknosa.jpg',
'P5': 'pusarium.jpg',
'P6': 'rebah_kecamba.jpeg',
'P7': 'virus kuning.jpg',
};
String _getSeverity(double confidence, double matchPercentage,
double totalWeight, int matchedSymptomsCount) {
final averageWeight =
matchedSymptomsCount > 0 ? totalWeight / matchedSymptomsCount : 0.0;
if (confidence >= 70 && (matchPercentage >= 0.5 || averageWeight >= 0.6)) {
return 'Kritis';
} else if (confidence >= 40 ||
(averageWeight >= 0.5 && matchPercentage >= 0.25)) {
return 'Perhatian';
} else if (confidence >= 20 || averageWeight >= 0.3) {
return 'Ringan';
} else {
return 'Minimal';
}
}
void _showDiagnosisReason(BuildContext context, double confidence,
int matchedSymptomsCount, int totalSymptoms) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
backgroundColor: Colors.white,
elevation: 10,
title: Row(
children: const [
Icon(
Icons.info_outline,
color: Color(0xFFAC2B36),
size: 28.0,
),
SizedBox(width: 10),
Text(
'Alasan Diagnosis',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36),
),
),
],
),
content: Text(
confidence >= 70
? 'Sistem mendeteksi banyak gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan tingkat kemungkinan tinggi (${confidence.toStringAsFixed(1)}%).'
: confidence >= 40
? 'Sistem mendeteksi beberapa gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan kemungkinan sedang (${confidence.toStringAsFixed(1)}%).'
: 'Sistem mendeteksi sedikit gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan kemungkinan rendah (${confidence.toStringAsFixed(1)}%).',
style: const TextStyle(
fontSize: 16.0,
color: Colors.black87,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Tutup',
style: TextStyle(fontSize: 16.0, color: Color(0xFF4CAF50)),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final confidence = result['confidence'] as double;
final matchPercentage = result['matchPercentage'] as double;
final matchedSymptoms =
(result['matchedSymptoms'] as List<dynamic>?)?.cast<String>() ?? [];
final totalWeight = result['totalWeight'] as double;
final matchedSymptomsCount = matchedSymptoms.length;
final totalSymptoms = disease.symptomIds.length;
final severity = _getSeverity(
confidence, matchPercentage, totalWeight, matchedSymptomsCount);
var solutionText = disease.solutions.join('\n');
if (severity == 'Kritis') {
solutionText += '\n(Tindakan mendesak diperlukan karena gejala kritis)';
} else if (severity == 'Perhatian') {
solutionText += '\n(Tindakan segera diperlukan)';
}
final recommendation = result['recommendation'] ?? '';
final prevention = severity == 'Kritis'
? 'Segera konsultasi dengan ahli pertanian dan lakukan pencegahan intensif.'
: 'Jaga kebersihan tanaman dan hindari kelembapan berlebih.';
final actionGuidance = severity == 'Kritis'
? 'Segera lakukan tindakan yang disarankan dan konsultasikan dengan ahli pertanian.'
: severity == 'Perhatian'
? 'Lakukan tindakan yang disarankan sesegera mungkin untuk mencegah penyebaran.'
: 'Amati tanaman secara rutin dan lakukan pencegahan dasar.';
final imageFileName = diseaseImageMap[disease.id] ?? 'placeholder.jpg';
return PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
Navigator.pop(context, true);
},
child: Scaffold(
appBar: AppBar(
title: Text(disease.name),
backgroundColor: Colors.white,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
titleTextStyle: const TextStyle(
color: Colors.black87,
fontSize: 20,
fontWeight: FontWeight.w600,
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context, true);
},
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'disease_image_${disease.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
'assets/$imageFileName',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 200,
color: Colors.grey[200],
child: const Center(
child: Text('Gambar tidak tersedia')),
);
},
),
),
),
const SizedBox(height: 20),
Row(
children: [
Text(
'Kemungkinan: ${confidence.toStringAsFixed(1)}%',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(width: 12),
Expanded(
child: LinearProgressIndicator(
value: confidence / 100,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
confidence >= 70
? const Color(0xFFAC2B36)
: confidence >= 40
? Colors.amber
: const Color(0xFF4CAF50),
),
minHeight: 6,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Text(
'Tingkat Bahaya: $severity',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: severity == 'Kritis'
? const Color(0xFFAC2B36)
: severity == 'Perhatian'
? Colors.amber
: const Color(0xFF4CAF50),
),
),
IconButton(
icon: Icon(
Icons.info_outline,
size: 20,
color: Colors.grey[600],
),
onPressed: () => _showDiagnosisReason(context, confidence,
matchedSymptomsCount, totalSymptoms),
),
],
),
const SizedBox(height: 12),
Text(
severity == 'Kritis'
? 'Penyakit ini sangat mungkin terjadi dan memerlukan tindakan segera.'
: severity == 'Perhatian'
? 'Penyakit ini kemungkinan ada. Segera ambil tindakan yang disarankan.'
: 'Penyakit ini mungkin ada, tetapi tidak terlalu serius. Lakukan pencegahan.',
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
),
if (matchedSymptoms.length < 2) ...[
const SizedBox(height: 12),
Text(
'Peringatan: Diagnosis berdasarkan sedikit gejala. Periksa gejala lain untuk hasil lebih akurat.',
style: TextStyle(
fontSize: 13,
color: const Color(0xFFAC2B36),
fontStyle: FontStyle.italic,
),
),
],
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.sick_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Gejala yang Cocok:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: disease.symptomIds.map((symptomId) {
final symptom = symptoms.firstWhere(
(s) => s.id == symptomId,
orElse: () => Symptom(
id: '', description: 'Tidak Diketahui', weight: 0.0),
);
if (matchedSymptoms.contains(symptomId)) {
return Chip(
label: Text(
symptom.description,
style: const TextStyle(fontSize: 12),
),
backgroundColor:
const Color(0xFF4CAF50).withOpacity(0.1),
labelStyle: const TextStyle(color: Color(0xFF4CAF50)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFF4CAF50), width: 1),
),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
);
}
return const SizedBox.shrink();
}).toList(),
),
if (disease.symptomIds.length > matchedSymptoms.length) ...[
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.search_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Gejala yang Perlu Diperiksa:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: disease.symptomIds.map((symptomId) {
final symptom = symptoms.firstWhere(
(s) => s.id == symptomId,
orElse: () => Symptom(
id: '',
description: 'Tidak Diketahui',
weight: 0.0),
);
if (!matchedSymptoms.contains(symptomId)) {
return Chip(
label: Text(
symptom.description,
style: const TextStyle(fontSize: 12),
),
backgroundColor: Colors.grey[100],
labelStyle: TextStyle(color: Colors.grey[600]),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side:
BorderSide(color: Colors.grey[400]!, width: 1),
),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
);
}
return const SizedBox.shrink();
}).toList(),
),
],
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.healing_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Solusi:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
solutionText,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
if (recommendation.isNotEmpty) ...[
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.lightbulb_outline,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Rekomendasi Tambahan:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
recommendation,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
],
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.shield_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Pencegahan:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
prevention,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.next_plan_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Langkah Selanjutnya:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
actionGuidance,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
],
),
),
),
),
);
}
}
class ResultPage extends StatefulWidget {
final Map<String, Map<String, dynamic>> results;
const ResultPage({Key? key, required this.results}) : super(key: key);
@override
State<ResultPage> createState() => _ResultPageState();
}
class _ResultPageState extends State<ResultPage> with TickerProviderStateMixin {
late List<Map<String, dynamic>> validResults;
late Map<String, Disease> diseaseMap;
late Map<String, Symptom> symptomMap;
late List<AnimationController> _controllers;
late List<Animation<double>> _fadeAnimations;
late TabController _tabController;
bool _isLoading = true;
int _selectedTabIndex = 0;
static const Map<String, String> diseaseImageMap = {
'P1': 'serkospora.jpg',
'P2': 'Alternaria.PNG',
'P3': 'Fitoftora.jpg',
'P4': 'antraknosa.jpg',
'P5': 'pusarium.jpg',
'P6': 'rebah_kecamba.jpeg',
'P7': 'virus kuning.jpg',
};
@override
void initState() {
super.initState();
if (kDebugMode) {
print('ResultPage initState: Memulai dengan results: ${widget.results}');
print('Apakah results kosong? ${widget.results.isEmpty}');
}
_initializeData();
}
Future<void> _initializeData() async {
try {
if (kDebugMode) {
print('Memulai _initializeData...');
print('Input results: ${widget.results}');
}
diseaseMap = {for (var disease in diseases) disease.id: disease};
symptomMap = {for (var symptom in symptoms) symptom.id: symptom};
validResults = [];
_controllers = [];
_fadeAnimations = [];
final tempResults = widget.results;
if (tempResults.isEmpty) {
if (kDebugMode) {
print('Results kosong, mengatur validResults sebagai list kosong.');
}
if (mounted) {
setState(() => _isLoading = false);
}
return;
}
for (var entry in tempResults.entries) {
if (kDebugMode) {
print('Memproses entry: ${entry.key} dengan value: ${entry.value}');
}
if (!entry.value.containsKey('matchedSymptoms') ||
entry.value['matchedSymptoms'] == null) {
if (kDebugMode) {
print(
'matchedSymptoms tidak ada untuk ${entry.key}, menambahkan default: []');
}
entry.value['matchedSymptoms'] = [];
}
entry.value['confidence'] =
(entry.value['confidence'] as num?)?.toDouble() ?? 0.0;
entry.value['matchPercentage'] =
(entry.value['matchPercentage'] as num?)?.toDouble() ?? 0.0;
entry.value['recommendation'] =
entry.value['recommendation']?.toString() ?? '';
entry.value['totalWeight'] =
(entry.value['totalWeight'] as num?)?.toDouble() ?? 0.0;
}
validResults = tempResults.entries
.where((entry) {
final isValid = diseaseMap.containsKey(entry.key) &&
entry.value['confidence'] is double &&
entry.value['matchPercentage'] is double &&
entry.value['totalWeight'] is double &&
entry.value['matchedSymptoms'] is List;
if (!isValid && kDebugMode) {
print('Entry tidak valid untuk ${entry.key}: ${entry.value}');
}
return isValid;
})
.map((entry) => {
'entry': entry,
'matchedSymptomsCount':
(entry.value['matchedSymptoms'] as List<dynamic>?)
?.length ??
0,
})
.toList()
..sort((a, b) {
final bConfidence =
(b['entry'] as MapEntry).value['confidence'] as double? ?? 0.0;
final aConfidence =
(a['entry'] as MapEntry).value['confidence'] as double? ?? 0.0;
final compareByScore = bConfidence.compareTo(aConfidence);
if (compareByScore != 0) return compareByScore;
final bMatched = b['matchedSymptomsCount'] as int? ?? 0;
final aMatched = a['matchedSymptomsCount'] as int? ?? 0;
return bMatched.compareTo(aMatched);
});
if (kDebugMode) {
print('Jumlah validResults: ${validResults.length}');
for (var result in validResults) {
print(
'Penyakit: ${result['entry'].key}, Confidence: ${result['entry'].value['confidence']}, Matched: ${result['matchedSymptomsCount']}');
}
}
_controllers = List.generate(
validResults.isEmpty ? 0 : validResults.length,
(index) => AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
),
);
_fadeAnimations = _controllers
.map((controller) => Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
))
.toList();
if (validResults.length > 3) {
_tabController = TabController(
length: validResults.length,
vsync: this,
initialIndex: _selectedTabIndex,
);
_tabController.addListener(() {
if (_tabController.index != _selectedTabIndex) {
setState(() {
_selectedTabIndex = _tabController.index;
});
}
});
}
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() => _isLoading = false);
if (kDebugMode) {
print('Inisialisasi selesai, _isLoading = false');
}
}
if (validResults.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var i = 0; i < _controllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 150), () {
if (mounted && !_controllers[i].isAnimating) {
_controllers[i].forward();
}
});
}
});
}
} catch (e, stackTrace) {
if (kDebugMode) {
print('Error di _initializeData: $e');
print('Stack trace: $stackTrace');
}
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal memuat hasil diagnosis: $e'),
backgroundColor: const Color(0xFFAC2B36),
),
);
}
}
}
@override
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
_tabController?.dispose();
super.dispose();
}
String _getSeverity(double confidence, double matchPercentage,
double totalWeight, int matchedSymptomsCount) {
final averageWeight =
matchedSymptomsCount > 0 ? totalWeight / matchedSymptomsCount : 0.0;
if (confidence >= 70 && (matchPercentage >= 0.5 || averageWeight >= 0.6)) {
return 'Kritis';
} else if (confidence >= 40 ||
(averageWeight >= 0.5 && matchPercentage >= 0.25)) {
return 'Perhatian';
} else if (confidence >= 20 || averageWeight >= 0.3) {
return 'Ringan';
} else {
return 'Minimal';
}
}
BoxDecoration _getCardDecoration(double totalWeight, double confidence,
double matchPercentage, int matchedSymptomsCount) {
final severity = _getSeverity(
confidence, matchPercentage, totalWeight, matchedSymptomsCount);
final borderColor = severity == 'Kritis'
? const Color(0xFFAC2B36).withOpacity(0.3)
: severity == 'Perhatian'
? Colors.amber.withOpacity(0.3)
: const Color(0xFF4CAF50).withOpacity(0.3);
return BoxDecoration(
color: Colors.white,
border: Border.all(color: borderColor, width: 1),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
);
}
void _showDiagnosisReason(BuildContext context, double confidence,
int matchedSymptomsCount, int totalSymptoms) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
backgroundColor: Colors.white,
elevation: 10,
title: Row(
children: const [
Icon(
Icons.info_outline,
color: Color(0xFFAC2B36),
size: 28.0,
),
SizedBox(width: 10),
Text(
'Alasan Diagnosis',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36),
),
),
],
),
content: Text(
confidence >= 70
? 'Sistem mendeteksi banyak gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan tingkat kemungkinan tinggi (${confidence.toStringAsFixed(1)}%).'
: confidence >= 40
? 'Sistem mendeteksi beberapa gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan kemungkinan sedang (${confidence.toStringAsFixed(1)}%).'
: 'Sistem mendeteksi sedikit gejala yang cocok ($matchedSymptomsCount dari $totalSymptoms) dengan kemungkinan rendah (${confidence.toStringAsFixed(1)}%).',
style: const TextStyle(
fontSize: 16.0,
color: Colors.black87,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Tutup',
style: TextStyle(fontSize: 16.0, color: Color(0xFF4CAF50)),
),
),
],
),
);
}
Future<bool> _showBackConfirmation(BuildContext context) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
backgroundColor: Colors.white,
elevation: 10,
title: Row(
children: const [
Icon(
Icons.warning_rounded,
color: Color(0xFFAC2B36),
size: 28.0,
),
SizedBox(width: 10),
Text(
'Konfirmasi Kembali',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36),
),
),
],
),
content: const Text(
'Kembali ke halaman diagnosis akan menghapus gejala yang dipilih. Apakah Anda yakin ingin melanjutkan?',
style: TextStyle(
fontSize: 16.0,
color: Colors.black87,
height: 1.5,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text(
'Batal',
style: TextStyle(fontSize: 16.0, color: Colors.grey),
),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
child: const Text(
'Kembali',
style: TextStyle(fontSize: 16.0, color: Colors.white),
),
),
],
),
);
return result ?? false;
}
Widget _buildDiseaseCard(int index) {
if (index >= _fadeAnimations.length) {
return const SizedBox.shrink();
}
final result = validResults[index]['entry'];
final disease = diseaseMap[result.key];
if (disease == null || result.value['confidence'] < 10) {
return const SizedBox.shrink();
}
final totalWeight = result.value['totalWeight'] as double;
final confidence = result.value['confidence'] as double;
final matchPercentage = result.value['matchPercentage'] as double;
final matchedSymptomsCount =
validResults[index]['matchedSymptomsCount'] as int;
final totalSymptoms = disease.symptomIds.length;
final matchedSymptomsList =
(result.value['matchedSymptoms'] as List<dynamic>?)?.cast<String>() ??
[];
final severity = _getSeverity(
confidence, matchPercentage, totalWeight, matchedSymptomsCount);
final matchedSymptoms = <String>[];
for (var symptomId in disease.symptomIds) {
if (matchedSymptomsList.contains(symptomId)) {
final symptom = symptomMap[symptomId] ??
Symptom(id: '', description: 'Tidak Diketahui', weight: 0.0);
matchedSymptoms.add(symptom.description);
}
}
final unmatchedSymptoms = <String>[];
for (var symptomId in disease.symptomIds) {
if (!matchedSymptomsList.contains(symptomId)) {
final symptom = symptomMap[symptomId] ??
Symptom(id: '', description: 'Tidak Diketahui', weight: 0.0);
unmatchedSymptoms.add(symptom.description);
}
}
final imageFileName = diseaseImageMap[disease.id] ?? 'placeholder.jpg';
var solutionText = disease.solutions.join('\n');
if (severity == 'Kritis') {
solutionText += '\n(Tindakan mendesak diperlukan karena gejala kritis)';
} else if (severity == 'Perhatian') {
solutionText += '\n(Tindakan segera diperlukan)';
}
final recommendation = result.value['recommendation'] ?? '';
final prevention = severity == 'Kritis'
? 'Segera konsultasi dengan ahli pertanian dan lakukan pencegahan intensif.'
: 'Jaga kebersihan tanaman dan hindari kelembapan berlebih.';
final actionGuidance = severity == 'Kritis'
? 'Segera lakukan tindakan yang disarankan dan konsultasikan dengan ahli pertanian.'
: severity == 'Perhatian'
? 'Lakukan tindakan yang disarankan sesegera mungkin untuk mencegah penyebaran.'
: 'Amati tanaman secara rutin dan lakukan pencegahan dasar.';
return FadeTransition(
opacity: _fadeAnimations[index],
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.1, 0),
end: Offset.zero,
).animate(_fadeAnimations[index]),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8),
constraints: BoxConstraints(
maxWidth:
MediaQuery.of(context).size.width > 600 ? 600 : double.infinity,
),
decoration: _getCardDecoration(
totalWeight, confidence, matchPercentage, matchedSymptomsCount),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
disease.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
Icon(
severity == 'Kritis'
? Icons.warning_amber_rounded
: severity == 'Perhatian'
? Icons.notifications_active
: Icons.check_circle_outline,
color: severity == 'Kritis'
? const Color(0xFFAC2B36)
: severity == 'Perhatian'
? Colors.amber
: const Color(0xFF4CAF50),
size: 24,
),
],
),
const SizedBox(height: 12),
Row(
children: [
Text(
'Kemungkinan: ${confidence.toStringAsFixed(1)}%',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(width: 12),
Expanded(
child: TweenAnimationBuilder(
tween: Tween<double>(begin: 0.0, end: confidence / 100),
duration: const Duration(milliseconds: 600),
builder: (context, value, child) {
return LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
confidence >= 70
? const Color(0xFFAC2B36)
: confidence >= 40
? Colors.amber
: const Color(0xFF4CAF50),
),
minHeight: 5,
borderRadius: BorderRadius.circular(4),
);
},
),
),
IconButton(
icon: Icon(
Icons.info_outline,
size: 20,
color: Colors.grey[600],
),
onPressed: () => _showDiagnosisReason(context, confidence,
matchedSymptomsCount, totalSymptoms),
),
],
),
const SizedBox(height: 12),
Text(
'Tingkat Bahaya: $severity',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: severity == 'Kritis'
? const Color(0xFFAC2B36)
: severity == 'Perhatian'
? Colors.amber
: const Color(0xFF4CAF50),
),
),
const SizedBox(height: 8),
Text(
severity == 'Kritis'
? 'Penyakit ini sangat mungkin terjadi dan memerlukan tindakan segera.'
: severity == 'Perhatian'
? 'Penyakit ini kemungkinan ada. Segera ambil tindakan yang disarankan.'
: 'Penyakit ini mungkin ada, tetapi tidak terlalu serius. Lakukan pencegahan.',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
if (matchedSymptoms.length < 2) ...[
const SizedBox(height: 12),
Text(
'Peringatan: Diagnosis berdasarkan sedikit gejala. Periksa gejala lain untuk hasil lebih akurat.',
style: TextStyle(
fontSize: 12,
color: const Color(0xFFAC2B36),
fontStyle: FontStyle.italic,
),
),
],
const SizedBox(height: 20),
Hero(
tag: 'disease_image_${disease.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
'assets/$imageFileName',
height: 80,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 80,
color: Colors.grey[200],
child: const Center(
child:
Icon(Icons.broken_image, color: Colors.grey)),
);
},
),
),
),
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.sick_outlined, size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Gejala yang Cocok:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
children: matchedSymptoms
.map((symptom) => Chip(
label: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 60),
child: Text(
symptom,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
),
backgroundColor:
const Color(0xFF4CAF50).withOpacity(0.1),
labelStyle: const TextStyle(color: Color(0xFF4CAF50)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFF4CAF50), width: 1),
),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
))
.toList(),
),
if (unmatchedSymptoms.isNotEmpty) ...[
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.search_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Gejala yang Perlu Diperiksa:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
children: unmatchedSymptoms
.map((symptom) => Chip(
label: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 60),
child: Text(
symptom,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
),
backgroundColor: Colors.grey[100],
labelStyle: TextStyle(color: Colors.grey[600]),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Colors.grey[400]!, width: 1),
),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
))
.toList(),
),
],
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.healing, size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Solusi:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
solutionText,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
if (recommendation.isNotEmpty) ...[
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.lightbulb_outline,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Rekomendasi Tambahan:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
recommendation,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
],
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.shield_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Pencegahan:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
prevention,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 20),
Row(
children: [
Icon(Icons.next_plan_outlined,
size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
const Text(
'Langkah Selanjutnya:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
actionGuidance,
style: const TextStyle(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 20),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiseaseDetailPage(
disease: disease,
result: result.value,
),
),
);
},
child: const Text(
'Lihat Detail Lengkap',
style: TextStyle(
fontSize: 14,
color: Color(0xFF4CAF50),
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
final confirmed = await _showBackConfirmation(context);
if (confirmed) {
Navigator.pop(context, true);
}
},
child: Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: const Text('Hasil Diagnosa'),
backgroundColor: Colors.white,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
titleTextStyle: const TextStyle(
color: Colors.black87,
fontSize: 20,
fontWeight: FontWeight.w600,
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
final confirmed = await _showBackConfirmation(context);
if (confirmed) {
Navigator.pop(context, true);
}
},
),
),
body: SafeArea(
child: _isLoading
? const Center(
child: CircularProgressIndicator(
color: Color(0xFFAC2B36),
strokeWidth: 3,
),
)
: validResults.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 80,
color: Colors.grey[300],
),
const SizedBox(height: 20),
Text(
'Tidak ada penyakit yang cocok dengan gejala yang dipilih.',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
onPressed: () async {
final confirmed =
await _showBackConfirmation(context);
if (confirmed) {
Navigator.pop(context, true);
}
},
child: const Text(
'Coba Diagnosa Lagi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
)
: validResults.length <= 3
? ListView.builder(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width >
600
? (MediaQuery.of(context).size.width - 600) /
2
: 16,
vertical: 16),
itemCount: validResults.length,
itemBuilder: (context, index) =>
_buildDiseaseCard(index),
)
: Column(
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 8),
margin: const EdgeInsets.only(bottom: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: const Color(0xFFAC2B36),
unselectedLabelColor: Colors.grey[600],
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(
color: Color(0xFFAC2B36),
width: 2,
),
insets:
EdgeInsets.symmetric(horizontal: 16),
),
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
),
padding: const EdgeInsets.symmetric(
horizontal: 16),
tabs:
validResults.asMap().entries.map((entry) {
final disease =
diseaseMap[entry.value['entry'].key]!;
return Tab(
text: disease.name,
);
}).toList(),
),
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children:
validResults.asMap().entries.map((entry) {
final controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
final animation =
Tween<double>(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
));
controller.forward();
return FadeTransition(
opacity: animation,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context)
.size
.width >
600
? (MediaQuery.of(context)
.size
.width -
600) /
2
: 16,
vertical: 16),
child: _buildDiseaseCard(entry.key),
),
);
}).toList(),
),
),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
height: 56,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
onPressed: () async {
final confirmed = await _showBackConfirmation(context);
if (confirmed) {
Navigator.pop(context, true);
}
},
child: const Text(
'Kembali ke Diagnosis',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
);
}
}