import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../../layout/main_layout.dart'; import '../ibu_drawer.dart'; // Import Dashboard Ibu agar navigasi PopScope berfungsi import '../dashboard_ibu.dart'; class GrafikAnakTBUPage extends StatefulWidget { const GrafikAnakTBUPage({super.key}); @override State createState() => _GrafikAnakTBUPageState(); } class _GrafikAnakTBUPageState extends State { List anakList = []; bool isLoading = true; @override void initState() { super.initState(); getData(); } Future getData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String userId = prefs.getString("id_user") ?? ""; if (userId.isEmpty) { if (mounted) setState(() => isLoading = false); return; } try { var response = await http.post( Uri.parse( "http://ta.myhost.id/E31230549/mposyandu_api/grafik_balita/get_grafik_tb_u.php"), body: {"user_id": userId}, ); var jsonData = json.decode(response.body); if (jsonData["success"] && jsonData["data"] != null) { setState(() { anakList = jsonData["data"]; isLoading = false; }); } else { setState(() => isLoading = false); } } catch (e) { if (mounted) setState(() => isLoading = false); } } int hitungUmurBulan(String tglLahir, String tglPeriksa) { try { DateTime lahir = DateTime.parse(tglLahir); DateTime periksa = DateTime.parse(tglPeriksa); int months = (periksa.year - lahir.year) * 12 + periksa.month - lahir.month; return months < 0 ? 0 : months; } catch (e) { return 0; } } String formatUsiaDetail(String tglLahir, String tglPeriksa) { try { DateTime birth = DateTime.parse(tglLahir); DateTime now = DateTime.parse(tglPeriksa); int years = now.year - birth.year; int months = now.month - birth.month; int days = now.day - birth.day; if (days < 0) { DateTime lastMonth = DateTime(now.year, now.month, 0); days += lastMonth.day; months--; } if (months < 0) { years--; months += 12; } return "$years Thn $months Bln $days Hari"; } catch (e) { return "-"; } } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; // ✅ FIX FINAL: selalu kembali ke Dashboard Ibu Navigator.pushAndRemoveUntil( context, MaterialPageRoute( builder: (_) => const DashboardIbuPage(), ), (route) => false, ); }, child: MainLayout( title: "", drawer: const IbuDrawer(), body: isLoading ? const Center(child: CircularProgressIndicator()) : anakList.isEmpty ? Center( child: Text("Tidak ada data pemeriksaan tb/u.", style: GoogleFonts.poppins(color: Colors.black))) : SingleChildScrollView( padding: const EdgeInsets.all(10), child: Column( children: [ // List Card per Anak ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: anakList.length, itemBuilder: (context, index) { var anak = anakList[index]; // Kondisi Jika pemeriksaan kosong if (anak["riwayat"] == null || anak["riwayat"].isEmpty) { return _buildNoExaminationCard( anak["nama"], anak["jenis_kelamin"]); } List spots = []; List> riwayatDetails = []; double lastTB = 0; int lastBulan = 0; String lastTgl = ""; for (var p in anak["riwayat"]) { double tbVal = double.tryParse(p["tb"].toString()) ?? 0.0; if (tbVal > 0) { int bulan = hitungUmurBulan( anak["tanggal_lahir"], p["tanggal_pemeriksaan"]); String usiaFormat = formatUsiaDetail( anak["tanggal_lahir"], p["tanggal_pemeriksaan"]); spots.add(FlSpot(bulan.toDouble(), tbVal)); riwayatDetails.add({ "x": bulan.toDouble(), "y": tbVal, "usia_lengkap": usiaFormat }); lastTB = tbVal; lastBulan = bulan; lastTgl = p["tanggal_pemeriksaan"]; } } if (spots.isEmpty) { return _buildNoExaminationCard( anak["nama"], anak["jenis_kelamin"]); } return GrafikCardKIA_TB( nama: anak["nama"], jk: anak["jenis_kelamin"], umurBulan: lastBulan, tb: lastTB, detailUsiaTerakhir: lastTgl.isNotEmpty ? formatUsiaDetail( anak["tanggal_lahir"], lastTgl) : "-", historySpots: spots, historyDetails: riwayatDetails, ); }, ), // Keterangan Warna Grafik _buildGlobalLegendNoCard(), ], ), ), ), ); } Widget _buildNoExaminationCard(String nama, String jk) { final bool isLaki = jk == "L"; final Color headerColor = isLaki ? Colors.blue.shade700 : Colors.pink; return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 20), clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), color: headerColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(nama.toUpperCase(), style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 12, color: Colors.white)), Text(isLaki ? "Laki-laki" : "Perempuan", style: GoogleFonts.poppins( fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)), ], ), ), Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40), color: const Color(0xFFF8F4F9), child: Text( "Belum ada riwayat pemeriksaan tinggi badan.", textAlign: TextAlign.center, style: GoogleFonts.poppins( color: Colors.black45, fontSize: 13, fontStyle: FontStyle.italic), ), ), ], ), ); } Widget _buildGlobalLegendNoCard() { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Panduan Membaca Grafik TB/U:", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), const SizedBox(height: 15), _legendRow(Colors.green, "Garis Hijau (Median)", "Menunjukkan tinggi badan rata-rata anak sesuai usianya. Jika posisi garis pertumbuhan anak berada di sekitar garis ini, pertumbuhan tinggi badan anak termasuk normal."), _legendRow(Colors.red, "Garis Merah (Batas Normal)", "Merupakan batas bawah dan batas atas kategori normal. Jika tinggi badan anak berada di antara dua garis merah, tinggi badan anak masih termasuk kategori normal sesuai usia."), _legendRow(Colors.black, "Garis Hitam (Batas Kritis)", "Menunjukkan batas di luar kategori normal. Jika tinggi badan berada di bawah garis hitam bawah, anak termasuk sangat pendek. Jika berada di atas garis hitam atas, anak termasuk lebih tinggi dari rata-rata."), ], ), ); } Widget _legendRow(Color color, String label, String desc) { return Padding( padding: const EdgeInsets.only(bottom: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 18, height: 4, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 12), Text(label, style: GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.black87)), ], ), const SizedBox(height: 5), Padding( padding: const EdgeInsets.only(left: 30), child: Text(desc, textAlign: TextAlign.justify, style: GoogleFonts.poppins( fontSize: 11, color: Colors.black54, height: 1.4)), ), ], ), ); } } class GrafikCardKIA_TB extends StatelessWidget { final String nama; final String jk; final int umurBulan; final double tb; final String detailUsiaTerakhir; final List historySpots; final List> historyDetails; const GrafikCardKIA_TB({ super.key, required this.nama, required this.jk, required this.umurBulan, required this.tb, required this.detailUsiaTerakhir, required this.historySpots, required this.historyDetails, }); double getSDValueAtAge(List spots, int targetUmur) { if (spots.isEmpty) return 0; for (int i = 0; i < spots.length - 1; i++) { if (targetUmur >= spots[i].x && targetUmur <= spots[i + 1].x) { double ratio = (targetUmur - spots[i].x) / (spots[i + 1].x - spots[i].x); return spots[i].y + ratio * (spots[i + 1].y - spots[i].y); } } return spots.last.y; } Map analisisStatusTB(bool isLaki, int umur, double tinggi) { double sd3 = getSDValueAtAge(_getSDRefTBU(isLaki, 3), umur); double sdM2 = getSDValueAtAge(_getSDRefTBU(isLaki, -2), umur); double sdM3 = getSDValueAtAge(_getSDRefTBU(isLaki, -3), umur); if (tinggi < sdM3) return { "status": "Sangat Pendek (Severely Stunted)", "warna": Colors.black }; if (tinggi < sdM2) return {"status": "Pendek (Stunted)", "warna": Colors.red}; if (tinggi > sd3) return {"status": "Tinggi", "warna": Colors.purple}; return {"status": "Normal", "warna": Colors.green}; } @override Widget build(BuildContext context) { final bool isLaki = jk == "L"; final Color headerColor = isLaki ? Colors.blue.shade700 : Colors.pink; final Color accentColor = isLaki ? Colors.blue.shade700 : const Color(0xFFE91E63); final analisis = analisisStatusTB(isLaki, umurBulan, tb); final Color statusColor = analisis["warna"]; final filteredSpots = historySpots.where((spot) => spot.x <= 60).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), margin: const EdgeInsets.only(bottom: 10), clipBehavior: Clip.antiAlias, child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), color: headerColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(nama.toUpperCase(), style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)), Text(isLaki ? "Laki-laki" : "Perempuan", style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.w500, fontSize: 12)), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(10, 25, 25, 20), child: SizedBox( width: double.infinity, height: 250, child: LineChart( LineChartData( lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (touchedSpot) => Colors.blueGrey.withOpacity(0.9), getTooltipItems: (List touchedSpots) { return touchedSpots.map((spot) { if (spot.barIndex != 5) return null; var detail = historyDetails.firstWhere( (element) => element["x"] == spot.x && element["y"] == spot.y, orElse: () => {"usia_lengkap": "?"}); return LineTooltipItem( "Data Balita\n", GoogleFonts.poppins( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold), children: [ TextSpan( text: "Tinggi: ${spot.y} cm\nUsia: ${detail["usia_lengkap"]}", style: GoogleFonts.poppins( color: Colors.white, fontSize: 10, fontWeight: FontWeight.normal)) ], ); }).toList(); }, ), ), minX: 0, maxX: 60, minY: 45, maxY: 125, titlesData: FlTitlesData( rightTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (val, _) => Text( val.toInt().toString(), style: GoogleFonts.poppins( fontSize: 10, color: Colors.black54)))), leftTitles: AxisTitles( axisNameWidget: Text("TB (cm)", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.bold)), sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (val, _) => Text( val.toInt().toString(), style: GoogleFonts.poppins( fontSize: 10, color: Colors.black54)))), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( axisNameWidget: Text("Usia (Bulan/Tahun)", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.bold)), sideTitles: SideTitles( showTitles: true, interval: 12, getTitlesWidget: (val, _) => Text( val == 0 ? "Lahir" : "${(val / 12).toInt()} Thn", style: GoogleFonts.poppins(fontSize: 9)))), ), gridData: const FlGridData( show: true, drawVerticalLine: true, horizontalInterval: 10, verticalInterval: 12), borderData: FlBorderData( show: true, border: Border.all(color: Colors.black12)), lineBarsData: [ _sdLine(_getSDRefTBU(isLaki, 3), Colors.black), _sdLine(_getSDRefTBU(isLaki, 2), Colors.red), _sdLine(_getSDRefTBU(isLaki, 0), Colors.green), _sdLine(_getSDRefTBU(isLaki, -2), Colors.red), _sdLine(_getSDRefTBU(isLaki, -3), Colors.black), LineChartBarData( spots: filteredSpots, isCurved: true, color: accentColor, barWidth: 4, dotData: FlDotData( show: true, getDotPainter: (spot, p, bar, i) => FlDotCirclePainter( radius: 6, color: Colors.white, strokeWidth: 3, strokeColor: accentColor))), ], ), ), ), ), ], ), ), Align( alignment: Alignment.centerLeft, child: SizedBox( width: MediaQuery.of(context).size.width * 0.75, child: Card( elevation: 4, clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), margin: const EdgeInsets.only(bottom: 25), color: statusColor, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Keterangan Warna Status Gizi:", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 10, color: Colors.white70)), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 4, children: [ _statusLegend(Colors.green, "Normal"), _statusLegend(Colors.red, "Pendek"), _statusLegend(Colors.black, "S. Pendek"), _statusLegend(Colors.purple, "Tinggi"), ], ), const Divider(color: Colors.white24, height: 20), RichText( text: TextSpan( style: GoogleFonts.poppins( fontSize: 12, color: Colors.white), children: [ const TextSpan( text: "Status Pertumbuhan: ", style: TextStyle(fontWeight: FontWeight.normal)), TextSpan( text: "${analisis["status"]}", style: const TextStyle(fontWeight: FontWeight.bold)), ], ), ), const SizedBox(height: 4), Text("Tinggi: $tb cm | Usia: $detailUsiaTerakhir", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w500, color: Colors.white.withOpacity(0.9))), ], ), ), ), ), ), ], ); } Widget _statusLegend(Color color, String text) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 7, height: 7, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 0.5))), const SizedBox(width: 4), Text(text, style: GoogleFonts.poppins(fontSize: 8, color: Colors.white70)), ], ); } LineChartBarData _sdLine(List spots, Color color) { return LineChartBarData( spots: spots, color: color.withOpacity(0.35), barWidth: 1.5, dotData: const FlDotData(show: false), isCurved: true); } List _getSDRefTBU(bool isLaki, int type) { if (isLaki) { switch (type) { case 3: return const [ FlSpot(0, 53.7), FlSpot(6, 71.9), FlSpot(12, 80.5), FlSpot(24, 93.9), FlSpot(36, 104.8), FlSpot(48, 113.9), FlSpot(60, 122.2) ]; case 2: return const [ FlSpot(0, 52.1), FlSpot(6, 70.1), FlSpot(12, 78.3), FlSpot(24, 91.3), FlSpot(36, 101.7), FlSpot(48, 110.5), FlSpot(60, 118.5) ]; case 0: return const [ FlSpot(0, 49.9), FlSpot(6, 67.6), FlSpot(12, 75.7), FlSpot(24, 87.8), FlSpot(36, 96.1), FlSpot(48, 103.3), FlSpot(60, 110.0) ]; case -2: return const [ FlSpot(0, 46.1), FlSpot(6, 61.2), FlSpot(12, 68.6), FlSpot(24, 79.1), FlSpot(36, 88.0), FlSpot(48, 94.9), FlSpot(60, 100.7) ]; case -3: return const [ FlSpot(0, 44.2), FlSpot(6, 58.0), FlSpot(12, 65.0), FlSpot(24, 74.9), FlSpot(36, 82.4), FlSpot(48, 88.9), FlSpot(60, 94.2) ]; } } else { switch (type) { case 3: return const [ FlSpot(0, 52.9), FlSpot(6, 70.3), FlSpot(12, 79.2), FlSpot(24, 92.9), FlSpot(36, 103.9), FlSpot(48, 113.3), FlSpot(60, 122.0) ]; case 2: return const [ FlSpot(0, 51.3), FlSpot(6, 68.5), FlSpot(12, 77.0), FlSpot(24, 90.1), FlSpot(36, 100.8), FlSpot(48, 109.8), FlSpot(60, 118.1) ]; case 0: return const [ FlSpot(0, 49.1), FlSpot(6, 65.7), FlSpot(12, 74.0), FlSpot(24, 86.4), FlSpot(36, 95.1), FlSpot(48, 102.7), FlSpot(60, 109.4) ]; case -2: return const [ FlSpot(0, 45.4), FlSpot(6, 60.1), FlSpot(12, 67.9), FlSpot(24, 79.0), FlSpot(36, 88.1), FlSpot(48, 95.2), FlSpot(60, 100.7) ]; case -3: return const [ FlSpot(0, 43.6), FlSpot(6, 57.3), FlSpot(12, 64.9), FlSpot(24, 75.2), FlSpot(36, 83.8), FlSpot(48, 90.3), FlSpot(60, 95.6) ]; } } return []; } }