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 GrafikAnakBBUsiaPage extends StatefulWidget { const GrafikAnakBBUsiaPage({super.key}); @override State createState() => _GrafikAnakIbuPageState(); } class _GrafikAnakIbuPageState 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") ?? ""; try { var response = await http.post( Uri.parse( "http://ta.myhost.id/E31230549/mposyandu_api/grafik_balita/get_grafik_balita.php"), body: {"user_id": userId}, ); var jsonData = json.decode(response.body); if (jsonData["success"] && jsonData["data"] != null && jsonData["data"].isNotEmpty) { Map groupedAnak = {}; for (var item in jsonData["data"]) { String id = item["id"].toString(); if (!groupedAnak.containsKey(id)) { groupedAnak[id] = { "nama": item["nama"], "jenis_kelamin": item["jenis_kelamin"], "tanggal_lahir": item["tanggal_lahir"], "pemeriksaan": [] }; } if (item["bb"] != null && item["bb"] != "-" && item["tanggal_pemeriksaan"] != null) { groupedAnak[id]["pemeriksaan"].add({ "tgl_periksa": item["tanggal_pemeriksaan"], "bb": double.tryParse(item["bb"].toString()) ?? 0.0 }); } } setState(() { anakList = groupedAnak.values.toList(); isLoading = false; }); } else { setState(() { anakList = []; isLoading = false; }); } } catch (e) { debugPrint("Error fetching data: $e"); setState(() => isLoading = false); } } int hitungUmurBulanPeriksa(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 formatUsiaLengkap(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()) : Padding( padding: const EdgeInsets.all(8.0), child: anakList.isEmpty ? _buildEmptyState("Tidak ada data pemeriksaan bb/u.") : SingleChildScrollView( child: Column( children: [ ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: anakList.length, itemBuilder: (context, index) { var anak = anakList[index]; if (anak["pemeriksaan"].isEmpty) { return _buildNoExaminationCard( anak["nama"], anak["jenis_kelamin"]); } List> detailSpots = []; List spots = []; double lastBB = 0; int lastBulan = 0; String lastTgl = ""; for (var p in anak["pemeriksaan"]) { int bulan = hitungUmurBulanPeriksa( anak["tanggal_lahir"], p["tgl_periksa"]); double berat = p["bb"]; String usiaFormat = formatUsiaLengkap( anak["tanggal_lahir"], p["tgl_periksa"]); spots.add(FlSpot(bulan.toDouble(), berat)); detailSpots.add({ "x": bulan.toDouble(), "y": berat, "usia_lengkap": usiaFormat, }); lastBB = berat; lastBulan = bulan; lastTgl = p["tgl_periksa"]; } return GrafikCardKIA( nama: anak["nama"], jk: anak["jenis_kelamin"], umurBulan: lastBulan, bb: lastBB, detailUsiaTerakhir: lastTgl.isNotEmpty ? formatUsiaLengkap( anak["tanggal_lahir"], lastTgl) : "-", historySpots: spots, historyDetails: detailSpots, ); }, ), _buildGlobalLegendNoCard(), ], ), ), ), ), ); } Widget _buildEmptyState(String pesan) { return Center( child: Text( pesan, textAlign: TextAlign.center, style: GoogleFonts.poppins(fontSize: 14, color: Colors.black), ), ); } 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: 16, vertical: 12), color: headerColor, child: Row( children: [ Text(nama.toUpperCase(), style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 12, color: Colors.white)), const Spacer(), Text(isLaki ? "Laki-laki" : "Perempuan", style: GoogleFonts.poppins(fontSize: 12, color: Colors.white)), ], ), ), Padding( padding: const EdgeInsets.all(20.0), child: Text( "Belum ada riwayat pemeriksaan berat badan.", textAlign: TextAlign.center, style: GoogleFonts.poppins( color: Colors.grey, 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("Keterangan Grafik BB/U:", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), const SizedBox(height: 15), _legendRow(Colors.green, "Garis Hijau (Ideal)", "Menunjukkan berat badan rata-rata anak sesuai usianya. Jika grafik berat badan anak berada di sekitar garis hijau, berarti pertumbuhan berat badan anak termasuk normal"), _legendRow(Colors.red, "Garis Merah (Batas Normal)", "Menunjukkan batas bawah dan batas atas kategori normal. Jika berat badan anak berada di antara dua garis merah, pertumbuhan berat badan masih termasuk kategori normal sesuai usia."), _legendRow(Colors.black, "Garis Hitam (Batas Waspada)", "Menunjukkan batas di luar kategori normal. Jika grafik berada di bawah garis hitam bawah, anak berisiko kekurangan gizi. Jika berada di atas garis hitam atas, berat badan anak berlebih."), ], ), ); } 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 extends StatelessWidget { final String nama; final String jk; final int umurBulan; final double bb; final String detailUsiaTerakhir; final List historySpots; final List> historyDetails; const GrafikCardKIA({ super.key, required this.nama, required this.jk, required this.umurBulan, required this.bb, 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 analisisGizi(bool isLaki, int umur, double bb) { double sd3 = getSDValueAtAge(_getSDData(isLaki, 3), umur); double sd2 = getSDValueAtAge(_getSDData(isLaki, 2), umur); double sdM2 = getSDValueAtAge(_getSDData(isLaki, -2), umur); double sdM3 = getSDValueAtAge(_getSDData(isLaki, -3), umur); if (bb > sd3) return {"status": "Sangat Berlebih (Obesitas)", "warna": Colors.purple}; if (bb > sd2) return {"status": "Berat Badan Lebih", "warna": Colors.orange}; if (bb >= sdM2) return {"status": "Berat Badan Normal", "warna": Colors.green}; if (bb >= sdM3) return {"status": "Berat Badan Kurang", "warna": Colors.red}; return {"status": "Risiko Gizi Buruk", "warna": Colors.black}; } @override Widget build(BuildContext context) { final bool isLaki = jk == "L"; final Color headerColor = isLaki ? Colors.blue : Colors.pink; final Color accentColor = isLaki ? Colors.blue.shade700 : const Color(0xFFE91E63); final analisis = analisisGizi(isLaki, umurBulan, bb); final Color statusColor = analisis["warna"]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( elevation: 4, clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), margin: const EdgeInsets.only(bottom: 10), child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 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, 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": "${spot.x.toInt()} Bulan"}, ); return LineTooltipItem( "Data Balita\n", GoogleFonts.poppins( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold), children: [ TextSpan( text: "Berat: ${spot.y} kg\nUsia: ${detail["usia_lengkap"]}", style: GoogleFonts.poppins( color: Colors.white, fontSize: 10, fontWeight: FontWeight.normal), ), ], ); }).toList(); }, ), ), minX: 0, maxX: 60, minY: 2, maxY: 30, 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("BB (kg)", 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: 5, verticalInterval: 12), borderData: FlBorderData( show: true, border: Border.all(color: Colors.black12)), lineBarsData: [ _sdLine(_getSDData(isLaki, 3), Colors.black), _sdLine(_getSDData(isLaki, 2), Colors.red), _sdLine(_getSDData(isLaki, 0), Colors.green), _sdLine(_getSDData(isLaki, -2), Colors.red), _sdLine(_getSDData(isLaki, -3), Colors.black), LineChartBarData( spots: historySpots, 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)), ), ], ), ), ), ), ], ), ), if (historySpots.isNotEmpty) 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, "Kurang"), _statusLegend(Colors.orange, "BB Lebih"), _statusLegend(Colors.purple, "Obesitas"), ], ), const Divider(color: Colors.white24, height: 20), RichText( text: TextSpan( style: GoogleFonts.poppins( fontSize: 12, color: Colors.white), children: [ const TextSpan( text: "Status Berat Badan: ", style: TextStyle(fontWeight: FontWeight.normal)), TextSpan( text: "${analisis["status"]}", style: const TextStyle( fontWeight: FontWeight.bold)), ], ), ), const SizedBox(height: 4), Text("BB Terakhir: $bb kg | 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 _getSDData(bool isLaki, int type) { if (isLaki) { if (type == 3) return const [ FlSpot(0, 5.0), FlSpot(6, 9.8), FlSpot(12, 13.3), FlSpot(24, 16.5), FlSpot(36, 19.7), FlSpot(48, 23.2), FlSpot(60, 27.0) ]; if (type == 2) return const [ FlSpot(0, 4.4), FlSpot(6, 9.0), FlSpot(12, 12.0), FlSpot(24, 15.0), FlSpot(36, 17.8), FlSpot(48, 21.0), FlSpot(60, 24.2) ]; if (type == 0) return const [ FlSpot(0, 3.3), FlSpot(6, 7.9), FlSpot(12, 9.6), FlSpot(24, 12.2), FlSpot(36, 14.3), FlSpot(48, 16.3), FlSpot(60, 18.3) ]; if (type == -2) return const [ FlSpot(0, 2.5), FlSpot(6, 6.4), FlSpot(12, 7.7), FlSpot(24, 9.7), FlSpot(36, 11.3), FlSpot(48, 12.7), FlSpot(60, 14.1) ]; if (type == -3) return const [ FlSpot(0, 2.1), FlSpot(6, 5.9), FlSpot(12, 7.0), FlSpot(24, 8.8), FlSpot(36, 10.3), FlSpot(48, 11.7), FlSpot(60, 13.0) ]; } else { if (type == 3) return const [ FlSpot(0, 4.8), FlSpot(6, 9.3), FlSpot(12, 12.9), FlSpot(24, 16.5), FlSpot(36, 20.2), FlSpot(48, 24.4), FlSpot(60, 29.0) ]; if (type == 2) return const [ FlSpot(0, 4.2), FlSpot(6, 8.5), FlSpot(12, 11.5), FlSpot(24, 14.8), FlSpot(36, 18.1), FlSpot(48, 21.5), FlSpot(60, 25.0) ]; if (type == 0) return const [ FlSpot(0, 3.2), FlSpot(6, 7.3), FlSpot(12, 8.9), FlSpot(24, 11.5), FlSpot(36, 13.9), FlSpot(48, 16.1), FlSpot(60, 18.2) ]; if (type == -2) return const [ FlSpot(0, 2.4), FlSpot(6, 5.8), FlSpot(12, 7.0), FlSpot(24, 9.0), FlSpot(36, 10.8), FlSpot(48, 12.3), FlSpot(60, 13.8) ]; if (type == -3) return const [ FlSpot(0, 2.0), FlSpot(6, 5.3), FlSpot(12, 6.3), FlSpot(24, 8.1), FlSpot(36, 9.8), FlSpot(48, 11.2), FlSpot(60, 12.5) ]; } return []; } }