1555 lines
61 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|