1435 lines
54 KiB
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|