NIM_E31222534/Androidnya/lib/screens/dashboard/stunting_screen.dart

1555 lines
61 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../models/stunting_model.dart';
import '../../controllers/stunting_controller.dart';
import '../../services/stunting_service.dart';
import '../../services/perkembangan_service.dart';
import '../../services/profile_service.dart';
import '../../services/auth_service.dart';
import '../../services/dashboard_service.dart';
import '../../services/api_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:math';
class StuntingScreen extends StatefulWidget {
final int? anakId;
StuntingScreen({this.anakId});
@override
_StuntingScreenState createState() => _StuntingScreenState();
}
class _StuntingScreenState extends State<StuntingScreen> with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final Map<String, TextEditingController> _controllers = {
'Nama Anak': TextEditingController(),
'Usia (bulan)': TextEditingController(),
'Tinggi Badan (cm)': TextEditingController(),
'Berat Badan (kg)': TextEditingController(),
};
bool _isLoading = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late StuntingController _stuntingController;
late StuntingService _stuntingService;
late PerkembanganService _perkembanganService;
late ProfileService _profileService;
late AuthService _authService;
late DashboardService _dashboardService;
String _selectedAnakName = '';
int? _selectedAnakId;
int? _selectedPerkembanganId;
String _selectedGender = 'L'; // Default laki-laki
// For date selection
DateTime _tanggalPemeriksaan = DateTime.now();
List<StuntingData> _riwayatStunting = [];
@override
void initState() {
super.initState();
_stuntingController = StuntingController();
_stuntingService = StuntingService();
_perkembanganService = PerkembanganService();
_profileService = ProfileService();
_authService = AuthService();
_dashboardService = DashboardService();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 1000),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(0.1, 1.0, curve: Curves.easeOut),
));
_animationController.forward();
// Inisialisasi anakId jika ada
_selectedAnakId = widget.anakId;
_initializeData();
}
Future<void> _initializeData() async {
setState(() {
_isLoading = true;
});
try {
final prefs = await SharedPreferences.getInstance();
// Coba refresh token terlebih dahulu untuk memastikan autentikasi berfungsi
try {
final tokenRefreshed = await _refreshToken();
print('Token refresh status: ${tokenRefreshed ? "berhasil" : "gagal"}');
} catch (e) {
print('Error saat refresh token: $e');
}
// Jika anakId tidak diset, coba ambil dari shared preferences
if (_selectedAnakId == null) {
// Coba dapatkan dari selected_anak_id
_selectedAnakId = prefs.getInt('selected_anak_id');
// Jika masih null, coba ambil dari last_selected_anak
if (_selectedAnakId == null) {
_selectedAnakId = prefs.getInt('last_selected_anak');
}
// Jika masih null, coba ambil dari anak_id_active
if (_selectedAnakId == null) {
_selectedAnakId = prefs.getInt('anak_id_active');
}
}
print('Selected anak ID: $_selectedAnakId');
// Jika masih null, coba ambil ID anak pertama dari daftar anak
if (_selectedAnakId == null) {
// Coba mendapatkan ID anak dari DashboardService
try {
final lastSelectedAnakId = await _dashboardService.getLastSelectedAnakId();
if (lastSelectedAnakId != null) {
_selectedAnakId = lastSelectedAnakId;
print('Berhasil mendapatkan ID anak dari DashboardService: $_selectedAnakId');
}
} catch (e) {
print('Error mengambil lastSelectedAnakId: $e');
}
}
// Jika memiliki ID anak, coba dapatkan data anak
if (_selectedAnakId != null) {
// Coba dapatkan data anak dari dashboard summary
try {
final dashboardData = await _dashboardService.getDashboardSummary(anakId: _selectedAnakId!);
if (dashboardData['success'] == true && dashboardData['data'] != null) {
final childData = dashboardData['data']['anak'] ?? {};
final statsData = dashboardData['data']['statistik'] ?? {};
final growthData = dashboardData['data']['pertumbuhan'] ?? {};
// Debug respons API
print('Debug respons dashboard:');
print('childData: $childData');
print('statsData: $statsData');
print('growthData: $growthData');
// Set nama anak
_selectedAnakName = childData['nama_anak'] ?? childData['nama'] ?? '';
_selectedGender = childData['jenis_kelamin'] ?? 'L';
// Set ke preferences
await prefs.setString('selected_anak_name', _selectedAnakName);
await prefs.setString('selected_anak_gender', _selectedGender);
// Pre-fill nama
_controllers['Nama Anak']!.text = _selectedAnakName;
// Try to get height and weight
double tinggiBadan = 0;
double beratBadan = 0;
String usia = '';
// Dari pertumbuhan terbaru
if (growthData is Map && growthData.isNotEmpty) {
print('Mencoba ambil data dari growthData: $growthData');
if (growthData.containsKey('tinggi_badan')) {
tinggiBadan = double.tryParse(growthData['tinggi_badan'].toString()) ?? 0;
print('Tinggi badan dari growthData: $tinggiBadan');
}
if (growthData.containsKey('berat_badan')) {
beratBadan = double.tryParse(growthData['berat_badan'].toString()) ?? 0;
print('Berat badan dari growthData: $beratBadan');
}
}
// Dari statistik
if (tinggiBadan <= 0 && statsData is Map && statsData.isNotEmpty) {
print('Mencoba ambil data dari statsData: $statsData');
if (statsData.containsKey('height') && statsData['height'] is Map) {
tinggiBadan = double.tryParse(statsData['height']['value'].toString()) ?? 0;
print('Tinggi badan dari statsData: $tinggiBadan');
} else if (statsData.containsKey('tinggi_badan')) {
tinggiBadan = double.tryParse(statsData['tinggi_badan'].toString()) ?? 0;
print('Tinggi badan dari statsData (tinggi_badan): $tinggiBadan');
}
if (statsData.containsKey('weight') && statsData['weight'] is Map) {
beratBadan = double.tryParse(statsData['weight']['value'].toString()) ?? 0;
print('Berat badan dari statsData: $beratBadan');
} else if (statsData.containsKey('berat_badan')) {
beratBadan = double.tryParse(statsData['berat_badan'].toString()) ?? 0;
print('Berat badan dari statsData (berat_badan): $beratBadan');
}
}
// Dari data anak
if (tinggiBadan <= 0 && childData is Map) {
print('Mencoba ambil data dari childData: $childData');
// Coba semua kemungkinan field untuk tinggi badan
final possibleTinggiFields = ['tinggi_badan', 'tinggi', 'height', 'tb'];
for (var field in possibleTinggiFields) {
if (childData.containsKey(field) && childData[field] != null) {
final nilai = childData[field].toString().trim();
if (nilai.isNotEmpty && nilai != '0' && nilai != 'null') {
final parsed = double.tryParse(nilai);
if (parsed != null && parsed > 0) {
tinggiBadan = parsed;
print('Tinggi badan dari childData[$field]: $tinggiBadan');
break;
}
}
}
}
// Coba semua kemungkinan field untuk berat badan
final possibleBeratFields = ['berat_badan', 'berat', 'weight', 'bb'];
for (var field in possibleBeratFields) {
if (childData.containsKey(field) && childData[field] != null) {
final nilai = childData[field].toString().trim();
if (nilai.isNotEmpty && nilai != '0' && nilai != 'null') {
final parsed = double.tryParse(nilai);
if (parsed != null && parsed > 0) {
beratBadan = parsed;
print('Berat badan dari childData[$field]: $beratBadan');
break;
}
}
}
}
}
// Jika masih belum dapat data, coba ambil dari semua property di dashboardData
if (tinggiBadan <= 0 || beratBadan <= 0) {
print('Mencari nilai TB dan BB di seluruh struktur data dashboard');
_searchValueInNestedMap(dashboardData, 'tinggi_badan', (value) {
if (tinggiBadan <= 0) {
final parsed = double.tryParse(value.toString());
if (parsed != null && parsed > 0) {
print('Menemukan tinggi_badan di struktur data: $parsed');
tinggiBadan = parsed;
}
}
});
_searchValueInNestedMap(dashboardData, 'berat_badan', (value) {
if (beratBadan <= 0) {
final parsed = double.tryParse(value.toString());
if (parsed != null && parsed > 0) {
print('Menemukan berat_badan di struktur data: $parsed');
beratBadan = parsed;
}
}
});
}
// Get usia
usia = childData['usia']?.toString() ?? statsData['age']?.toString() ?? childData['umur_bulan']?.toString() ?? '';
// Pre-fill form fields
if (tinggiBadan > 0) {
_controllers['Tinggi Badan (cm)']!.text = tinggiBadan.toString();
await prefs.setDouble('latest_tinggi_badan_${_selectedAnakId}', tinggiBadan);
}
if (beratBadan > 0) {
_controllers['Berat Badan (kg)']!.text = beratBadan.toString();
await prefs.setDouble('latest_berat_badan_${_selectedAnakId}', beratBadan);
}
// Jika masih belum mendapatkan tinggi dan berat, coba ambil dari API perkembangan
if ((tinggiBadan <= 0 || beratBadan <= 0) && _selectedAnakId != null) {
try {
print('Mencoba ambil data dari API perkembangan karena TB/BB masih 0');
final perkembanganList = await _perkembanganService.getPerkembanganByAnakId(_selectedAnakId!);
if (perkembanganList.isNotEmpty) {
// Sort berdasarkan tanggal terbaru
perkembanganList.sort((a, b) {
final dateA = a['tanggal'] != null ? DateTime.parse(a['tanggal'].toString()) : DateTime(2000);
final dateB = b['tanggal'] != null ? DateTime.parse(b['tanggal'].toString()) : DateTime(2000);
return dateB.compareTo(dateA); // Descending (terbaru dulu)
});
// Ambil data perkembangan terbaru
final latestData = perkembanganList.first;
print('Data perkembangan terbaru: $latestData');
// Simpan ID perkembangan untuk referensi
_selectedPerkembanganId = latestData['id'];
if (tinggiBadan <= 0 && latestData.containsKey('tinggi_badan')) {
final height = double.tryParse(latestData['tinggi_badan'].toString());
if (height != null && height > 0) {
tinggiBadan = height;
_controllers['Tinggi Badan (cm)']!.text = height.toString();
await prefs.setDouble('latest_tinggi_badan_${_selectedAnakId}', height);
print('Tinggi badan dari perkembangan: $height');
}
}
if (beratBadan <= 0 && latestData.containsKey('berat_badan')) {
final weight = double.tryParse(latestData['berat_badan'].toString());
if (weight != null && weight > 0) {
beratBadan = weight;
_controllers['Berat Badan (kg)']!.text = weight.toString();
await prefs.setDouble('latest_berat_badan_${_selectedAnakId}', weight);
print('Berat badan dari perkembangan: $weight');
}
}
} else {
print('Tidak ada data perkembangan untuk anak ini');
}
} catch (e) {
print('Error saat mengambil data perkembangan: $e');
}
}
if (usia.isNotEmpty) {
// Coba ekstrak angka bulan dari string usia
final RegExp regExp = RegExp(r'(\d+)');
final match = regExp.firstMatch(usia);
if (match != null) {
_controllers['Usia (bulan)']!.text = match.group(1) ?? '';
} else {
_controllers['Usia (bulan)']!.text = usia;
}
await prefs.setString('latest_usia_${_selectedAnakId}', usia);
}
print('Berhasil mendapatkan data anak dari dashboard: ${_selectedAnakName}, TB: $tinggiBadan, BB: $beratBadan');
}
} catch (e) {
print('Error mengambil dashboard summary: $e');
// Jika gagal mendapatkan data dari API, coba ambil data dari SharedPreferences
_fallbackToStoredData();
}
} else {
print('Tidak ada ID anak yang ditemukan, mencoba ambil dari user_data');
// Tidak ada ID anak yang ditemukan, coba ambil dari user_data
await _fetchChildDataFromUserData();
}
// Ambil riwayat stunting jika ada ID anak yang dipilih
if (_selectedAnakId != null) {
try {
_riwayatStunting = await _stuntingService.getStuntingByAnakId(_selectedAnakId!);
// Urutkan data stunting berdasarkan tanggal terbaru
_riwayatStunting.sort((a, b) => b.tanggalPemeriksaan.compareTo(a.tanggalPemeriksaan));
// Debug urutan data dari API
if (_riwayatStunting.isNotEmpty) {
print('Setelah pengurutan di _initializeData, urutan data:');
for (int i = 0; i < min(3, _riwayatStunting.length); i++) {
print('${i+1}. Tanggal: ${_riwayatStunting[i].tanggalPemeriksaan}, Status: ${_riwayatStunting[i].status}');
}
}
// Tambahkan nama anak ke riwayat stunting yang tidak memiliki nama
if (_riwayatStunting.isNotEmpty) {
_riwayatStunting = _riwayatStunting.map((data) {
if (data.namaPasien == null || data.namaPasien!.isEmpty) {
return StuntingData(
id: data.id,
anakId: data.anakId,
perkembanganId: data.perkembanganId,
tanggalPemeriksaan: data.tanggalPemeriksaan,
usia: data.usia,
tinggiBadan: data.tinggiBadan,
beratBadan: data.beratBadan,
status: data.status,
catatan: data.catatan,
gender: data.gender,
namaPasien: _selectedAnakName,
lingkarKepala: data.lingkarKepala,
);
}
return data;
}).toList();
}
} catch (e) {
print('Error mengambil riwayat stunting: $e');
}
}
} catch (e) {
print('Error dalam _initializeData: $e');
// Coba ambil data dari SharedPreferences jika ada kesalahan
_fallbackToStoredData();
} finally {
setState(() {
_isLoading = false;
});
}
}
// Metode untuk mengambil data anak dari user_data yang tersimpan
Future<void> _fetchChildDataFromUserData() async {
try {
final prefs = await SharedPreferences.getInstance();
final userData = prefs.getString('user_data');
if (userData != null && userData.contains('"anak"')) {
print('Mencoba mendapatkan data anak dari user_data...');
try {
final parsedUserData = json.decode(userData);
if (parsedUserData != null &&
parsedUserData['anak'] != null &&
parsedUserData['anak'] is List &&
(parsedUserData['anak'] as List).isNotEmpty) {
final children = parsedUserData['anak'] as List;
if (children.isNotEmpty) {
// Ambil data anak pertama
final firstChild = children.first;
if (firstChild != null && firstChild['id'] != null) {
_selectedAnakId = firstChild['id'];
_selectedAnakName = firstChild['nama'] ?? '';
_selectedGender = firstChild['jenis_kelamin'] ?? 'L';
// Simpan data anak ke SharedPreferences
await prefs.setInt('selected_anak_id', _selectedAnakId!);
await prefs.setString('selected_anak_name', _selectedAnakName);
await prefs.setString('selected_anak_gender', _selectedGender);
print('Berhasil mendapatkan ID anak dari user_data: $_selectedAnakId, nama: $_selectedAnakName');
// Pre-fill nama
_controllers['Nama Anak']!.text = _selectedAnakName;
// Fetch anak data manually dari user_data
if (firstChild['tinggi_badan'] != null || firstChild['tinggi'] != null) {
final tinggiStr = firstChild['tinggi_badan'] ?? firstChild['tinggi'] ?? '0';
final tinggi = double.tryParse(tinggiStr.toString()) ?? 0.0;
if (tinggi > 0) {
_controllers['Tinggi Badan (cm)']!.text = tinggi.toString();
await prefs.setDouble('latest_tinggi_badan_${_selectedAnakId}', tinggi);
}
}
if (firstChild['berat_badan'] != null || firstChild['berat'] != null) {
final beratStr = firstChild['berat_badan'] ?? firstChild['berat'] ?? '0';
final berat = double.tryParse(beratStr.toString()) ?? 0.0;
if (berat > 0) {
_controllers['Berat Badan (kg)']!.text = berat.toString();
await prefs.setDouble('latest_berat_badan_${_selectedAnakId}', berat);
}
}
if (firstChild['umur_bulan'] != null || firstChild['usia'] != null) {
final usiaStr = firstChild['umur_bulan'] ?? firstChild['usia'] ?? '';
_controllers['Usia (bulan)']!.text = usiaStr.toString();
await prefs.setString('latest_usia_${_selectedAnakId}', usiaStr.toString());
}
}
}
}
} catch (e) {
print('Error parsing user_data: $e');
}
} else {
print('user_data tidak ditemukan atau tidak berisi data anak');
_fallbackToStoredData();
}
} catch (e) {
print('Error dalam _fetchChildDataFromUserData: $e');
_fallbackToStoredData();
}
}
// Metode untuk menggunakan data yang sudah tersimpan di SharedPreferences
void _fallbackToStoredData() async {
try {
final prefs = await SharedPreferences.getInstance();
// Coba ambil data tersimpan untuk anak yang dipilih
if (_selectedAnakId != null) {
print('Menggunakan data tersimpan untuk anak $_selectedAnakId');
// Ambil nama anak
_selectedAnakName = prefs.getString('selected_anak_name') ?? '';
_selectedGender = prefs.getString('selected_anak_gender') ?? 'L';
if (_selectedAnakName.isNotEmpty) {
_controllers['Nama Anak']!.text = _selectedAnakName;
}
// Ambil tinggi, berat, dan usia tersimpan
final tinggi = prefs.getDouble('latest_tinggi_badan_${_selectedAnakId}');
final berat = prefs.getDouble('latest_berat_badan_${_selectedAnakId}');
final usia = prefs.getString('latest_usia_${_selectedAnakId}');
if (tinggi != null && tinggi > 0) {
_controllers['Tinggi Badan (cm)']!.text = tinggi.toString();
}
if (berat != null && berat > 0) {
_controllers['Berat Badan (kg)']!.text = berat.toString();
}
if (usia != null && usia.isNotEmpty) {
_controllers['Usia (bulan)']!.text = usia;
}
} else {
print('Tidak ada ID anak yang dipilih untuk fallback data');
}
} catch (e) {
print('Error dalam _fallbackToStoredData: $e');
}
}
// Refresh token untuk memastikan autentikasi berfungsi
Future<bool> _refreshToken() async {
try {
// Gunakan getCurrentUser yang sudah ada di AuthService untuk refresh token secara tidak langsung
final result = await _authService.getCurrentUser();
if (result['success'] == true) {
print('Token berhasil di-refresh melalui getCurrentUser');
return true;
} else {
// Jika getCurrentUser gagal, coba dengan pendekatan alternatif
final prefs = await SharedPreferences.getInstance();
final savedNik = prefs.getString('nik');
final savedPassword = prefs.getString('saved_password');
if (savedNik != null && savedPassword != null) {
try {
final loginResult = await _authService.login(
nik: savedNik,
password: savedPassword,
);
if (loginResult['success'] == true) {
print('Token berhasil di-refresh melalui login ulang');
return true;
}
} catch (e) {
print('Error saat login ulang: $e');
}
}
print('Gagal refresh token');
return false;
}
} catch (e) {
print('Error saat refresh token: $e');
return false;
}
}
@override
void dispose() {
_controllers.forEach((key, controller) => controller.dispose());
_animationController.dispose();
super.dispose();
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _tanggalPemeriksaan,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: ThemeData.light().copyWith(
colorScheme: ColorScheme.light(
primary: Colors.red.shade700,
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null && picked != _tanggalPemeriksaan) {
setState(() {
_tanggalPemeriksaan = picked;
});
}
}
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
// Jika anakId tidak ada, tampilkan error
if (_selectedAnakId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: Data anak tidak tersedia'),
backgroundColor: Colors.red,
),
);
setState(() => _isLoading = false);
return;
}
// Get form values
double? tinggi = double.tryParse(_controllers['Tinggi Badan (cm)']!.text);
String usia = _controllers['Usia (bulan)']!.text;
double? berat = double.tryParse(_controllers['Berat Badan (kg)']!.text);
// Check usia requirement - hanya anak berusia 24-60 bulan yang bisa dicek
int? usiaInt = int.tryParse(usia);
if (usiaInt == null || usiaInt < 24 || usiaInt > 60) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pemeriksaan stunting hanya untuk anak berusia 24-60 bulan'),
backgroundColor: Colors.red,
duration: Duration(seconds: 5),
),
);
setState(() => _isLoading = false);
return;
}
// Jika tidak ada perkembanganId, gunakan nilai default
if (_selectedPerkembanganId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Peringatan: Data perkembangan terbaru tidak tersedia. Hasilnya mungkin tidak akurat.'),
backgroundColor: Colors.orange,
),
);
}
if (tinggi != null && usia.isNotEmpty && berat != null) {
// Hitung status stunting via API
final result = await _stuntingService.calculateStuntingStatus(
usia: usia,
tinggiBadan: tinggi,
beratBadan: berat,
);
String status;
if (result['success'] == true) {
status = result['finalStatus'];
print('Status stunting: $status');
} else {
// Jika API gagal, gunakan perhitungan lokal
status = _stuntingController.getZScoreStatus(tinggi, usiaInt, _selectedGender);
print('API gagal, menggunakan status lokal: $status');
}
// Simpan data stunting ke API
final storeResult = await _stuntingService.createStuntingData(
anakId: _selectedAnakId!,
tanggal: _tanggalPemeriksaan.toIso8601String().split('T')[0],
usia: usia,
perkembanganId: _selectedPerkembanganId ?? 0, // Gunakan 0 jika tidak ada perkembanganId
status: status,
);
if (storeResult['success'] == true) {
print('Data stunting berhasil disimpan');
} else {
print('Gagal menyimpan data stunting: ${storeResult['message']}');
}
// Perbarui riwayat stunting
_riwayatStunting = await _stuntingService.getStuntingByAnakId(_selectedAnakId!);
// Urutkan riwayat stunting berdasarkan tanggal terbaru
_riwayatStunting.sort((a, b) => b.tanggalPemeriksaan.compareTo(a.tanggalPemeriksaan));
// Tambahkan nama anak ke riwayat stunting yang tidak memiliki nama
if (_riwayatStunting.isNotEmpty) {
_riwayatStunting = _riwayatStunting.map((data) {
if (data.namaPasien == null || data.namaPasien!.isEmpty) {
return StuntingData(
id: data.id,
anakId: data.anakId,
perkembanganId: data.perkembanganId,
tanggalPemeriksaan: data.tanggalPemeriksaan,
usia: data.usia,
tinggiBadan: data.tinggiBadan,
beratBadan: data.beratBadan,
status: data.status,
catatan: data.catatan,
gender: data.gender,
namaPasien: _selectedAnakName,
lingkarKepala: data.lingkarKepala,
);
}
return data;
}).toList();
}
// Tampilkan dialog hasil (menggunakan status dari API jika tersedia)
_showResultDialog(status, tinggi, berat, usiaInt);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: Data tidak valid'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
print('Error during form submission: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan: $e'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
void _showResultDialog(String status, double tinggi, double berat, int usia) {
final statusDetails = _stuntingService.getStatusDetails(status);
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Row(
children: [
Icon(
statusDetails['icon'] == 'warning'
? Icons.warning
: (statusDetails['icon'] == 'warning_amber'
? Icons.warning_amber
: Icons.check_circle),
color: Color(statusDetails['color']),
),
SizedBox(width: 8),
Text('Hasil Pemeriksaan', style: TextStyle(fontSize: 18)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: $status',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(statusDetails['color']),
),
),
SizedBox(height: 8),
Text('Tinggi: $tinggi cm'),
Text('Berat: $berat kg'),
Text('Usia: $usia bulan'),
SizedBox(height: 16),
Text(statusDetails['message']),
],
),
actions: [
Container(
width: double.infinity,
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.symmetric(vertical: 12),
),
onPressed: () => Navigator.pop(context),
child: Text('TUTUP', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
backgroundColor: Colors.red.shade700,
elevation: 0,
title: Text(
'Stunting',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
actions: [
// Tambahkan tombol riwayat di AppBar
if (_riwayatStunting.isNotEmpty)
Container(
margin: EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.history_edu,
color: Colors.white,
size: 26,
),
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
constraints: BoxConstraints(
minWidth: 14,
minHeight: 14,
),
child: Text(
_riwayatStunting.length.toString(),
style: TextStyle(
color: Colors.red.shade900,
fontSize: 8,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
onPressed: () => _showRiwayatDialog(),
tooltip: 'Lihat Riwayat Stunting',
),
),
],
),
body: _isLoading ?
Center(child: CircularProgressIndicator(color: Colors.red.shade700)) :
RefreshIndicator(
color: Colors.red.shade700,
onRefresh: () async {
// Tampilkan indikator memuat
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Memperbarui data...'),
duration: Duration(seconds: 1),
backgroundColor: Colors.red.shade700,
),
);
// Reset form dan muat ulang data
await _initializeData();
// Notifikasi selesai
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data berhasil diperbarui'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
},
child: FadeTransition(
opacity: _fadeAnimation,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.red.shade700,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.red.shade200.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 10,
offset: Offset(0, 3),
),
],
),
padding: EdgeInsets.fromLTRB(20, 0, 20, 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Form Pengukuran',
style: TextStyle(
fontSize: screenSize.width * 0.055,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
_selectedAnakName.isNotEmpty
? 'Lengkapi form di bawah untuk mendeteksi stunting $_selectedAnakName'
: 'Lengkapi form di bawah untuk mendeteksi stunting',
style: TextStyle(
fontSize: screenSize.width * 0.035,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Card(
elevation: 8,
shadowColor: Colors.grey.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Date selector
Text(
"Tanggal Pemeriksaan",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
SizedBox(height: 8),
InkWell(
onTap: () => _selectDate(context),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Icons.calendar_today, color: Colors.red.shade700),
SizedBox(width: 12),
Text(
'${_tanggalPemeriksaan.day}/${_tanggalPemeriksaan.month}/${_tanggalPemeriksaan.year}',
style: TextStyle(fontSize: 16),
),
],
),
),
),
SizedBox(height: 24),
// Form fields with better grouping
_buildFormSection("Data Anak"),
// Form fields
..._controllers.entries.map((entry) {
final isNumberField = entry.key.contains('(kg)') ||
entry.key.contains('(cm)') ||
entry.key.contains('(bulan)');
IconData fieldIcon;
if (entry.key.contains('Nama')) {
fieldIcon = Icons.person;
} else if (entry.key.contains('Usia')) {
fieldIcon = Icons.calendar_month;
} else if (entry.key.contains('Tinggi')) {
fieldIcon = Icons.height;
} else if (entry.key.contains('Berat')) {
fieldIcon = Icons.monitor_weight;
} else {
fieldIcon = Icons.note;
}
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextFormField(
controller: entry.value,
decoration: InputDecoration(
labelText: entry.key,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.red.shade700, width: 2),
),
prefixIcon: Icon(fieldIcon, color: Colors.red.shade700),
floatingLabelStyle: TextStyle(color: Colors.red.shade700),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
filled: true,
fillColor: Colors.grey.shade100, // Semua field berwarna abu-abu
enabled: false, // Semua field tidak bisa diubah
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
readOnly: true, // Semua field read only
keyboardType: isNumberField ? TextInputType.number : TextInputType.text,
maxLines: 1,
inputFormatters: isNumberField
? [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))]
: null,
validator: (value) {
if (value == null || value.isEmpty) {
return '${entry.key} tidak boleh kosong';
}
if (isNumberField && value != null && value.isNotEmpty) {
try {
final numValue = double.parse(value);
if (entry.key.contains('Tinggi')) {
if (numValue < 30 || numValue > 200) {
return 'Tinggi badan harus antara 30-200 cm';
}
}
if (entry.key.contains('Berat')) {
if (numValue < 1 || numValue > 100) {
return 'Berat badan harus antara 1-100 kg';
}
}
if (entry.key.contains('Usia')) {
if (numValue < 0 || numValue > 60) {
return 'Usia harus antara 0-60 bulan';
}
}
} catch (e) {
return 'Masukkan angka yang valid';
}
}
return null;
},
),
);
}).toList(),
SizedBox(height: 24),
// Submit button
Container(
width: double.infinity,
height: 54,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
),
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.save),
SizedBox(width: 8),
Text(
'SIMPAN & ANALISIS',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
),
),
),
// Info panel
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Card(
color: Colors.blue.shade50,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 2,
shadowColor: Colors.blue.withOpacity(0.3),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline,
color: Colors.blue.shade700,
size: 36,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Apa itu stunting?',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue.shade900,
),
),
SizedBox(height: 6),
Text(
'Stunting adalah kondisi gagal tumbuh pada anak akibat kekurangan gizi kronis dan infeksi berulang yang terjadi pada 1000 hari pertama kehidupan.',
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade900,
),
),
],
),
),
],
),
),
),
),
],
),
),
),
),
);
}
// Helper untuk membuat section pada form
Widget _buildFormSection(String title) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.red.shade800,
),
),
Divider(
color: Colors.red.shade200,
thickness: 1,
),
SizedBox(height: 16),
],
);
}
// Tambahkan metode baru untuk menampilkan dialog riwayat
void _showRiwayatDialog() {
if (_riwayatStunting.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tidak ada riwayat pemeriksaan'))
);
return;
}
// Pastikan data diurutkan berdasarkan tanggal terbaru sebelum ditampilkan
List<StuntingData> sortedData = List.from(_riwayatStunting);
sortedData.sort((a, b) => b.tanggalPemeriksaan.compareTo(a.tanggalPemeriksaan));
// Debug urutan data
print('Data terurut - urutan 3 data pertama:');
for (int i = 0; i < min(3, sortedData.length); i++) {
print('${i+1}. Tanggal: ${sortedData[i].tanggalPemeriksaan}, Status: ${sortedData[i].status}');
}
// Variabel untuk pagination
int _currentPage = 0;
final int _itemsPerPage = 3;
final int _totalPages = (sortedData.length / _itemsPerPage).ceil();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
// Hitung range data untuk halaman saat ini
final startIndex = _currentPage * _itemsPerPage;
final endIndex = min(startIndex + _itemsPerPage, sortedData.length);
final currentItems = sortedData.sublist(startIndex, endIndex);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(
Icons.history_edu,
color: Colors.red.shade700,
size: 24,
),
),
SizedBox(width: 10),
Text(
'Riwayat Pemeriksaan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Spacer(),
IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
Divider(),
// Daftar item
Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
),
child: ListView.builder(
shrinkWrap: true,
itemCount: currentItems.length,
itemBuilder: (context, index) {
final data = currentItems[index];
return _buildRiwayatStuntingDialogItem(data);
},
),
),
// Pagination controls
if (_totalPages > 1)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Tombol Previous
IconButton(
icon: Icon(Icons.arrow_back_ios, size: 18),
onPressed: _currentPage > 0
? () {
setState(() {
_currentPage--;
});
}
: null,
color: _currentPage > 0 ? Colors.red.shade700 : Colors.grey,
),
// Indikator halaman
Text(
'Halaman ${_currentPage + 1} dari $_totalPages',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
// Tombol Next
IconButton(
icon: Icon(Icons.arrow_forward_ios, size: 18),
onPressed: _currentPage < _totalPages - 1
? () {
setState(() {
_currentPage++;
});
}
: null,
color: _currentPage < _totalPages - 1 ? Colors.red.shade700 : Colors.grey,
),
],
),
),
],
),
),
);
},
),
);
}
// Widget untuk item riwayat dalam dialog
Widget _buildRiwayatStuntingDialogItem(StuntingData data) {
final statusDetails = _stuntingService.getStatusDetails(data.status);
// Gunakan nama anak yang sedang aktif jika namaPasien null pada data
final namaPasien = data.namaPasien ?? _selectedAnakName;
return Card(
elevation: 2,
margin: EdgeInsets.symmetric(vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Color(statusDetails['color']).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: Icon(
statusDetails['icon'] == 'warning'
? Icons.warning
: (statusDetails['icon'] == 'warning_amber'
? Icons.warning_amber
: Icons.check_circle),
color: Color(statusDetails['color']),
size: 24,
),
),
),
title: Text(
'${namaPasien.isNotEmpty ? namaPasien : _selectedAnakName}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status: ${data.status}',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Color(statusDetails['color']),
),
),
Text(
'Tanggal: ${_formatDate(data.tanggalPemeriksaan)}',
style: TextStyle(fontSize: 13),
),
Text(
'TB: ${data.tinggiBadan} cm | BB: ${data.beratBadan} kg | Usia: ${data.usia}',
style: TextStyle(fontSize: 13),
),
],
),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
// Tutup dialog riwayat
Navigator.pop(context);
// Tampilkan detail stunting
_showStuntingDetailDialog(data);
},
),
);
}
// Format tanggal
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
// Dialog untuk menampilkan detail stunting yang dipilih
void _showStuntingDetailDialog(StuntingData data) {
final statusDetails = _stuntingService.getStatusDetails(data.status);
// Gunakan nama anak yang sedang aktif jika namaPasien null pada data
final namaPasien = data.namaPasien ?? _selectedAnakName;
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Row(
children: [
Icon(
statusDetails['icon'] == 'warning'
? Icons.warning
: (statusDetails['icon'] == 'warning_amber'
? Icons.warning_amber
: Icons.check_circle),
color: Color(statusDetails['color']),
),
SizedBox(width: 8),
Expanded(
child: Text('Detail Pemeriksaan', style: TextStyle(fontSize: 18)),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tanggal: ${_formatDate(data.tanggalPemeriksaan)}',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
SizedBox(height: 8),
Text(
'Status: ${data.status}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(statusDetails['color']),
),
),
SizedBox(height: 12),
_detailRow('Nama', namaPasien.isNotEmpty ? namaPasien : _selectedAnakName),
_detailRow('Tinggi Badan', '${data.tinggiBadan} cm'),
_detailRow('Berat Badan', '${data.beratBadan} kg'),
_detailRow('Usia', '${data.usia} bulan'),
SizedBox(height: 16),
Text(
'Keterangan:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
SizedBox(height: 4),
Text(statusDetails['message']),
if (data.catatan != null && data.catatan!.isNotEmpty) ...[
SizedBox(height: 8),
Text(
'Catatan Dokter:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
SizedBox(height: 4),
Text(data.catatan!),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Tutup'),
),
],
),
);
}
// Helper untuk row pada detail dialog
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
),
SizedBox(width: 8),
Expanded(
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
// Helper untuk mencari nilai dalam struktur data bersarang
void _searchValueInNestedMap(Map<String, dynamic> map, String key, Function(dynamic value) callback) {
if (map.containsKey(key)) {
callback(map[key]);
}
for (var value in map.values) {
if (value is Map<String, dynamic>) {
_searchValueInNestedMap(value, key, callback);
} else if (value is List) {
for (var item in value) {
if (item is Map<String, dynamic>) {
_searchValueInNestedMap(item, key, callback);
}
}
}
}
}
}