TIF_NGANJUK_E41211992/lib/pages/diagnosis_page.dart

1026 lines
42 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'dart:math';
import '../data/disease_data.dart';
import '../logic/fuzzy_mamdani.dart';
import '../models/disease.dart';
import '../models/symptom.dart';
import 'result_page.dart';
class DiagnosisPage extends StatefulWidget {
const DiagnosisPage({super.key});
@override
State<DiagnosisPage> createState() => _DiagnosisPageState();
}
class _DiagnosisPageState extends State<DiagnosisPage>
with TickerProviderStateMixin {
final FuzzyMamdani fuzzyMamdani = FuzzyMamdani();
Map<String, double> selectedSymptoms = {};
List<Symptom> filteredSymptoms = symptoms;
final TextEditingController _searchController = TextEditingController();
final TextEditingController _historySearchController =
TextEditingController();
bool isLoading = false;
List<Map<String, dynamic>> history = [];
List<Map<String, dynamic>> filteredHistory = [];
late AnimationController _scaleController;
late AnimationController _dotController;
late AnimationController _textScaleController;
late Animation<double> _scaleAnimation;
late Animation<int> _dotAnimation;
late Animation<double> _textScaleAnimation;
late AnimationController _countScaleController;
late Animation<double> _countScaleAnimation;
late TabController _tabController;
final List<AnimationController> _fadeControllers = [];
final List<AnimationController> _slideControllers = [];
String sortOrder = 'Terbaru';
final List<String> loadingMessages = [
'Menganalisis gejala...',
'Memeriksa tanaman...',
'Mendiagnosis...',
'Memproses data...',
];
final Map<int, bool> expandedSymptoms = {};
@override
void initState() {
super.initState();
_searchController.addListener(filterAndSortSymptoms);
_historySearchController.addListener(filterHistory);
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(() {
if (mounted) setState(() {});
});
_scaleController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.easeOutBack),
);
_dotController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_dotAnimation = IntTween(begin: 0, end: 3).animate(
CurvedAnimation(parent: _dotController, curve: Curves.linear),
);
_textScaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)..repeat(reverse: true);
_textScaleAnimation = Tween<double>(begin: 0.9, end: 1.1).animate(
CurvedAnimation(parent: _textScaleController, curve: Curves.easeInOut),
);
_countScaleController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_countScaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _countScaleController, curve: Curves.elasticOut),
);
_loadHistory();
}
Future<void> _loadHistory() async {
try {
final prefs = await SharedPreferences.getInstance();
final historyData = prefs.getStringList('diagnosis_history') ?? [];
List<Map<String, dynamic>> tempHistory = [];
for (var item in historyData) {
var entry = jsonDecode(item) as Map<String, dynamic>;
var results = entry['results'] as Map<String, dynamic>;
var updatedResults = Map<String, Map<String, dynamic>>.from(results);
for (var resultEntry in updatedResults.entries) {
if (!resultEntry.value.containsKey('matchedSymptoms')) {
resultEntry.value['matchedSymptoms'] =
(entry['selectedSymptoms'] as Map<String, dynamic>)
.keys
.toList();
}
resultEntry.value['confidence'] =
resultEntry.value['confidence'] ?? 0.0;
resultEntry.value['matchPercentage'] =
resultEntry.value['matchPercentage'] ?? 0.0;
resultEntry.value['recommendation'] =
resultEntry.value['recommendation'] ?? '';
resultEntry.value['totalWeight'] =
resultEntry.value['totalWeight'] ?? 0.0;
}
entry['results'] = updatedResults;
tempHistory.add(entry);
}
if (mounted) {
setState(() {
history = tempHistory;
filteredHistory = List.from(history);
sortHistory();
_fadeControllers.clear();
_slideControllers.clear();
for (var i = 0; i < history.length; i++) {
final fadeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)..forward();
final slideController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)..forward();
_fadeControllers.add(fadeController);
_slideControllers.add(slideController);
}
});
}
} catch (e) {
if (mounted) {
setState(() {
history = [];
filteredHistory = [];
});
}
}
}
Future<void> _saveHistory(Map<String, Map<String, dynamic>> results) async {
try {
final prefs = await SharedPreferences.getInstance();
final timestamp = DateTime.now().toIso8601String();
final updatedResults = Map<String, Map<String, dynamic>>.from(results);
for (var entry in updatedResults.entries) {
if (!entry.value.containsKey('matchedSymptoms')) {
entry.value['matchedSymptoms'] = selectedSymptoms.keys.toList();
}
}
final historyEntry = {
'timestamp': timestamp,
'selectedSymptoms': selectedSymptoms.map((key, value) =>
MapEntry(key, symptoms.firstWhere((s) => s.id == key).description)),
'results': updatedResults.map(
(key, value) => MapEntry(key, Map<String, dynamic>.from(value))),
};
final historyData = prefs.getStringList('diagnosis_history') ?? [];
historyData.add(jsonEncode(historyEntry));
if (historyData.length > 50) historyData.removeAt(0);
await prefs.setStringList('diagnosis_history', historyData);
await _loadHistory();
} catch (e) {}
}
Future<void> _deleteHistoryEntry(int index) async {
final confirmed = 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',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36))),
],
),
content: const Text('Apakah Anda yakin ingin menghapus riwayat ini?',
style:
TextStyle(fontSize: 16.0, color: Colors.black87, height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Batal',
style: TextStyle(fontSize: 16.0, color: Colors.grey)),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
child: const Text('Hapus',
style: TextStyle(fontSize: 16.0, color: Colors.white)),
),
],
),
);
if (confirmed == true) {
try {
final prefs = await SharedPreferences.getInstance();
final historyData = prefs.getStringList('diagnosis_history') ?? [];
final reversedIndex = history.length - 1 - index;
historyData.removeAt(reversedIndex);
await prefs.setStringList('diagnosis_history', historyData);
await _loadHistory();
} catch (e) {}
}
}
Future<void> _clearHistory() async {
final confirmed = 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',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36))),
],
),
content: const Text('Apakah Anda yakin ingin menghapus semua riwayat?',
style:
TextStyle(fontSize: 16.0, color: Colors.black87, height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Batal',
style: TextStyle(fontSize: 16.0, color: Colors.grey)),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
child: const Text('Hapus',
style: TextStyle(fontSize: 16.0, color: Colors.white)),
),
],
),
);
if (confirmed == true) {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('diagnosis_history');
if (mounted) {
setState(() {
history = [];
filteredHistory = [];
_fadeControllers.clear();
_slideControllers.clear();
});
}
} catch (e) {}
}
}
void filterAndSortSymptoms() {
final query = _searchController.text.trim().toLowerCase();
if (mounted) {
setState(() {
filteredSymptoms = query.isEmpty
? symptoms
: symptoms
.where((symptom) =>
symptom.description.toLowerCase().contains(query))
.toList();
});
}
}
void filterHistory() {
final query = _historySearchController.text.trim().toLowerCase();
if (mounted) {
setState(() {
filteredHistory = query.isEmpty
? List.from(history)
: history.where((entry) {
final results = entry['results'] as Map<String, dynamic>;
final topDisease = results.entries
.where((e) => e.value['confidence'] >= 10)
.toList()
..sort((a, b) =>
b.value['confidence'].compareTo(a.value['confidence']));
final diseaseName = topDisease.isNotEmpty
? diseases
.firstWhere(
(d) => d.id == topDisease[0].key,
orElse: () => Disease(
id: '',
name: 'Tidak Diketahui',
symptomIds: [],
solutions: []),
)
.name
.toLowerCase()
: 'tidak diketahui';
return diseaseName.contains(query);
}).toList();
sortHistory();
});
}
}
void sortHistory() {
if (mounted) {
setState(() {
filteredHistory.sort((a, b) {
final dateA = DateTime.parse(a['timestamp']);
final dateB = DateTime.parse(b['timestamp']);
return sortOrder == 'Terbaru'
? dateB.compareTo(dateA)
: dateA.compareTo(dateB);
});
});
}
}
void toggleSymptom(Symptom symptom) {
if (mounted) {
setState(() {
if (selectedSymptoms.containsKey(symptom.id)) {
selectedSymptoms.remove(symptom.id);
} else {
selectedSymptoms[symptom.id] = symptom.weight;
}
_countScaleController.forward(from: 0.0);
});
}
}
void onDiagnose() async {
if (selectedSymptoms.isEmpty) {
_scaleController.forward(from: 0.0);
await showDialog(
context: context,
builder: (context) => ScaleTransition(
scale: _scaleAnimation,
child: AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0)),
backgroundColor: Colors.white,
elevation: 10,
title: Row(
children: const [
Icon(Icons.local_florist, color: Color(0xFFAC2B36), size: 28.0),
SizedBox(width: 10),
Text(
'Peringatan',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36)),
),
],
),
content: const Text(
'Pilih setidaknya satu gejala untuk melanjutkan.',
style:
TextStyle(fontSize: 16.0, color: Colors.black87, height: 1.5),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 8.0, bottom: 8.0),
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4CAF50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
child: const Text(
'OK',
style: TextStyle(
fontSize: 16.0,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
return;
}
setState(() {
isLoading = true;
});
_scaleController.forward(from: 0.0);
_textScaleController.forward(from: 0.0);
final randomMessage =
loadingMessages[Random().nextInt(loadingMessages.length)];
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.2),
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: FadeTransition(
opacity: _scaleAnimation,
child: Container(
width: 180,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.0),
border: Border.all(color: const Color(0xFFAC2B36), width: 1.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
color: Color(0xFFAC2B36), strokeWidth: 2.5),
const SizedBox(height: 8),
AnimatedBuilder(
animation: _textScaleController,
builder: (context, child) {
String dots = '.' * (_dotAnimation.value % 4);
return Transform.scale(
scale: _textScaleAnimation.value,
child: Text(
'$randomMessage$dots',
style: const TextStyle(
fontSize: 13.0,
fontWeight: FontWeight.w600,
color: Color(0xFFAC2B36)),
textAlign: TextAlign.center,
),
);
},
),
],
),
),
),
),
);
final results = fuzzyMamdani.diagnose(selectedSymptoms);
await _saveHistory(results);
await Future.delayed(const Duration(seconds: 3));
Navigator.pop(context);
setState(() {
isLoading = false;
});
final shouldRefresh = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => ResultPage(results: results)),
);
if (mounted) {
setState(() {
if (shouldRefresh == true) {
selectedSymptoms.clear();
_countScaleController.forward(from: 0.0);
}
sortHistory();
if (_tabController.index == 1) filterHistory();
});
}
}
Future<bool> _showExitConfirmation() async {
return 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 Keluar',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Color(0xFFAC2B36))),
],
),
content: const Text(
'Apakah Anda yakin ingin keluar dari aplikasi? Perubahan yang belum disimpan akan hilang.',
style:
TextStyle(fontSize: 16.0, color: Colors.black87, height: 1.5),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Batal',
style: TextStyle(fontSize: 16.0, color: Colors.grey)),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
child: const Text('Keluar',
style: TextStyle(fontSize: 16.0, color: Colors.white)),
),
],
),
) ??
false;
}
@override
void dispose() {
_searchController.dispose();
_historySearchController.dispose();
_scaleController.dispose();
_dotController.dispose();
_textScaleController.dispose();
_countScaleController.dispose();
_tabController.dispose();
for (var controller in _fadeControllers) controller.dispose();
for (var controller in _slideControllers) controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldExit = await _showExitConfirmation();
if (shouldExit && mounted) SystemNavigator.pop();
},
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 2,
iconTheme: const IconThemeData(color: Colors.black),
title: const Text('RawitCare',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
centerTitle: true,
bottom: TabBar(
controller: _tabController,
labelColor: const Color(0xFFAC2B36),
unselectedLabelColor: Colors.grey,
indicatorColor: const Color(0xFFAC2B36),
tabs: const [
Tab(text: 'Diagnosis'),
Tab(text: 'Riwayat'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Cari gejala...',
prefixIcon:
const Icon(Icons.search, color: Colors.grey),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon:
const Icon(Icons.clear, color: Colors.grey),
onPressed: () => _searchController.clear(),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide: const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide:
const BorderSide(color: Color(0xFF4CAF50)),
),
contentPadding:
const EdgeInsets.symmetric(vertical: 10.0),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ScaleTransition(
scale: _countScaleAnimation,
child: Text(
'Gejala yang dipilih: ${selectedSymptoms.length}',
style:
const TextStyle(fontSize: 14.0, color: Colors.grey),
),
),
),
Expanded(
child: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16.0),
itemCount: filteredSymptoms.length,
itemBuilder: (context, index) {
final symptom = filteredSymptoms[index];
final isSelected =
selectedSymptoms.containsKey(symptom.id);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: GestureDetector(
onTap: () => toggleSymptom(symptom),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 16.0, horizontal: 20.0),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF4CAF50)
: Colors.white,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: const Color(0xFF4CAF50), width: 1.0),
),
child: Row(
children: [
Icon(
isSelected
? Icons.check_circle
: Icons.circle_outlined,
color:
isSelected ? Colors.white : Colors.grey,
),
const SizedBox(width: 10),
Expanded(
child: Text(
symptom.description,
style: TextStyle(
fontSize: 16.0,
color: isSelected
? Colors.white
: Colors.black),
),
),
],
),
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
height: 50.0,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
onPressed: isLoading
? null
: () {
HapticFeedback.lightImpact();
onDiagnose();
},
child: const Text('Diagnosa',
style:
TextStyle(fontSize: 16.0, color: Colors.white)),
),
),
),
],
),
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _historySearchController,
decoration: InputDecoration(
hintText: 'Cari penyakit...',
prefixIcon:
const Icon(Icons.search, color: Colors.grey),
suffixIcon:
_historySearchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear,
color: Colors.grey),
onPressed: () =>
_historySearchController.clear(),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide:
const BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
borderSide:
const BorderSide(color: Color(0xFF4CAF50)),
),
contentPadding:
const EdgeInsets.symmetric(vertical: 10.0),
),
),
),
const SizedBox(width: 8),
DropdownButton<String>(
value: sortOrder,
items: ['Terbaru', 'Terlama'].map((order) {
return DropdownMenuItem(
value: order,
child: Text(
order,
style: TextStyle(
fontSize: 14.0,
color: Colors.black87,
fontWeight: sortOrder == order
? FontWeight.bold
: FontWeight.normal,
),
),
);
}).toList(),
onChanged: (value) {
if (mounted) {
setState(() {
sortOrder = value!;
sortHistory();
});
}
},
underline: Container(),
icon:
const Icon(Icons.sort, color: Color(0xFFAC2B36)),
dropdownColor: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
],
),
),
if (filteredHistory.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFAC2B36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
),
onPressed: _clearHistory,
child: const Text('Hapus Semua Riwayat',
style:
TextStyle(fontSize: 16.0, color: Colors.white)),
),
),
Expanded(
child: filteredHistory.isEmpty
? Center(
child: Text(
_historySearchController.text.isEmpty
? 'Belum ada riwayat diagnosis.'
: 'Tidak ada riwayat yang cocok dengan pencarian.',
style: const TextStyle(
fontSize: 16.0, color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: filteredHistory.length,
itemBuilder: (context, index) {
final entry = filteredHistory[index];
final results =
entry['results'] as Map<String, dynamic>;
final topDisease = results.entries
.where((e) => e.value['confidence'] >= 10)
.toList()
..sort((a, b) => b.value['confidence']
.compareTo(a.value['confidence']));
final diseaseName = topDisease.isNotEmpty
? diseases
.firstWhere(
(d) => d.id == topDisease[0].key,
orElse: () => Disease(
id: '',
name: 'Tidak Diketahui',
symptomIds: [],
solutions: []),
)
.name
: 'Tidak Diketahui';
final confidence = topDisease.isNotEmpty
? topDisease[0].value['confidence']
: 0.0;
final confidenceStr =
confidence.toStringAsFixed(1);
final symptomsList = (entry['selectedSymptoms']
as Map<String, dynamic>)
.values
.toList();
final isExpanded =
expandedSymptoms[index] ?? false;
return FadeTransition(
opacity: _fadeControllers[index]
.drive(Tween(begin: 0.0, end: 1.0)),
child: SlideTransition(
position: _slideControllers[index].drive(
Tween<Offset>(
begin: const Offset(0.1, 0),
end: Offset.zero)),
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0)),
margin: const EdgeInsets.symmetric(
vertical: 8.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(10.0),
),
child: Column(
children: [
ListTile(
contentPadding:
const EdgeInsets.all(16.0),
leading: Tooltip(
message: confidence >= 70
? 'Risiko Tinggi'
: 'Kondisi Stabil',
child: Icon(
confidence >= 70
? Icons
.local_fire_department
: Icons.local_florist,
color: confidence >= 70
? const Color(0xFFAC2B36)
: const Color(0xFF4CAF50),
size: 28.0,
),
),
title: Text(
'$diseaseName ($confidenceStr%)',
style: const TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Container(
padding: const EdgeInsets
.symmetric(
horizontal: 8.0,
vertical: 4.0),
decoration: BoxDecoration(
color:
const Color(0xFFAC2B36)
.withOpacity(0.1),
borderRadius:
BorderRadius.circular(
6.0),
),
child: Text(
'Tanggal: ${DateTime.parse(entry['timestamp']).toLocal().toString().substring(0, 16)}',
style: const TextStyle(
fontSize: 14.0,
color: Colors.black87,
fontWeight:
FontWeight.w500),
),
),
const SizedBox(height: 4),
Text(
'Gejala: ${symptomsList.join(', ')}',
style: const TextStyle(
fontSize: 14.0,
color: Colors.black54),
maxLines:
isExpanded ? null : 2,
overflow: isExpanded
? null
: TextOverflow.ellipsis,
),
],
),
trailing: IconButton(
icon: const Icon(Icons.delete,
color: Color(0xFFAC2B36)),
onPressed: () =>
_deleteHistoryEntry(index),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ResultPage(
results: Map<
String,
Map<String,
dynamic>>.from(
results)),
),
).catchError((error) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'Gagal membuka detail: $error'),
backgroundColor:
Colors.red),
);
});
},
),
if (symptomsList.join(', ').length >
50)
Padding(
padding: const EdgeInsets.only(
bottom: 8.0),
child: TextButton(
onPressed: () {
if (mounted) {
setState(() {
expandedSymptoms[index] =
!isExpanded;
});
}
},
child: Text(
isExpanded
? 'Sembunyikan'
: 'Lihat Selengkapnya',
style: const TextStyle(
color: Color(0xFF4CAF50),
fontSize: 14.0),
),
),
),
],
),
),
),
),
);
},
),
),
],
),
],
),
),
),
);
}
}