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 GrafikAnakBBTBPage extends StatefulWidget { const GrafikAnakBBTBPage({super.key}); @override State createState() => _GrafikAnakBBTBPageState(); } class _GrafikAnakBBTBPageState 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_bb_tb.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); } } String formatUsiaDetail(String tglLahir, String tglPeriksa) { 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"; } @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 ? const Center(child: Text("Tidak ada data pemeriksaan BB/TB.")) : SingleChildScrollView( padding: const EdgeInsets.all(10), child: Column( children: [ // List Card Grafik dan Status per Anak ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: anakList.length, itemBuilder: (context, index) { var anak = anakList[index]; List spots = []; List> riwayatDetails = []; double lastBB = 0; double lastTB = 0; String lastTgl = ""; if (anak["riwayat_bbtb"] != null && anak["riwayat_bbtb"].isNotEmpty) { for (var p in anak["riwayat_bbtb"]) { double bb = double.tryParse(p["bb"].toString()) ?? 0.0; double tb = double.tryParse(p["tb"].toString()) ?? 0.0; if (bb > 0 && tb >= 45 && tb <= 120) { String usiaFormat = formatUsiaDetail( anak["tanggal_lahir"], p["tanggal_pemeriksaan"]); spots.add(FlSpot(tb, bb)); riwayatDetails.add({ "x": tb, "y": bb, "usia_lengkap": usiaFormat }); lastBB = bb; lastTB = tb; lastTgl = p["tanggal_pemeriksaan"]; } } } return GrafikCardKIA_BBTB( nama: anak["nama"], jk: anak["jenis_kelamin"], bb: lastBB, tb: lastTB, detailUsiaTerakhir: lastTgl.isNotEmpty ? formatUsiaDetail( anak["tanggal_lahir"], lastTgl) : "-", historySpots: spots, historyDetails: riwayatDetails, ); }, ), // Keterangan Warna Grafik (Tanpa Card) _buildGlobalLegendNoCard(), ], ), ), ), ); } 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 BB/TB:", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), const SizedBox(height: 15), _legendRow(Colors.green, "Garis Hijau (Ideal)", "Menunjukkan berat badan ideal sesuai tinggi badan anak"), _legendRow(Colors.red, "Garis Merah (Batas Normal)", "Batas normal pertumbuhan. Jika titik anak masih di antara garis merah atas dan bawah, maka status gizi masih normal."), _legendRow(Colors.black, "Garis Hitam (Batas Waspada)", "Batas waspada. Jika titik berada di luar garis hitam, sebaiknya segera konsultasi ke bidan atau tenaga kesehatan."), ], ), ); } 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_BBTB extends StatelessWidget { final String nama; final String jk; final double bb; final double tb; final String detailUsiaTerakhir; final List historySpots; final List> historyDetails; const GrafikCardKIA_BBTB({ super.key, required this.nama, required this.jk, required this.bb, required this.tb, required this.detailUsiaTerakhir, required this.historySpots, required this.historyDetails, }); double getSDValueAtTB(List spots, double targetTB) { if (spots.isEmpty) return 0; if (targetTB <= spots.first.x) return spots.first.y; if (targetTB >= spots.last.x) return spots.last.y; for (int i = 0; i < spots.length - 1; i++) { if (targetTB >= spots[i].x && targetTB <= spots[i + 1].x) { double ratio = (targetTB - 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 analisisStatusBBTB(bool isLaki, double tb, double bb) { if (bb == 0) return {"status": "Belum ada data", "warna": Colors.grey}; double sd3 = getSDValueAtTB(_getSDRefBBTB_Gabungan(isLaki, 3), tb); double sd2 = getSDValueAtTB(_getSDRefBBTB_Gabungan(isLaki, 2), tb); double sdM2 = getSDValueAtTB(_getSDRefBBTB_Gabungan(isLaki, -2), tb); double sdM3 = getSDValueAtTB(_getSDRefBBTB_Gabungan(isLaki, -3), tb); if (bb < sdM3) return {"status": "Sangat Kurus (Gizi Buruk)", "warna": Colors.black}; if (bb < sdM2) return {"status": "Kurus (Gizi Kurang)", "warna": Colors.red}; if (bb > sd3) return {"status": "Obesitas", "warna": Colors.purple}; if (bb > sd2) return {"status": "Berat Badan Lebih", "warna": Colors.orange}; 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 analisis = analisisStatusBBTB(isLaki, tb, bb); final Color statusColor = analisis["warna"]; 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)), ], ), ), const Divider(height: 1, thickness: 1, color: Colors.black12), if (historySpots.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40), color: Colors.white, child: Text("Belum ada riwayat pemeriksaan.", textAlign: TextAlign.center, style: GoogleFonts.poppins( color: Colors.black45, fontStyle: FontStyle.italic, fontSize: 13)), ) else 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: "TB: ${spot.x} cm\nBB: ${spot.y} kg\nUsia: ${detail["usia_lengkap"]}", style: GoogleFonts.poppins( color: Colors.white, fontSize: 10, fontWeight: FontWeight.normal)) ], ); }).toList(); }, ), ), minX: 45, maxX: 120, minY: 2, maxY: 32, titlesData: FlTitlesData( rightTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 4, 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, interval: 4, 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("Tinggi Badan (cm)", style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.bold)), sideTitles: SideTitles( showTitles: true, interval: 10, getTitlesWidget: (val, _) => Text( val.toInt().toString(), style: GoogleFonts.poppins(fontSize: 9)))), ), gridData: const FlGridData( show: true, drawVerticalLine: true, horizontalInterval: 2, verticalInterval: 5), borderData: FlBorderData( show: true, border: Border.all(color: Colors.black12)), lineBarsData: [ _sdLine( _getSDRefBBTB_Gabungan(isLaki, 3), Colors.black), _sdLine( _getSDRefBBTB_Gabungan(isLaki, 2), Colors.red), _sdLine( _getSDRefBBTB_Gabungan(isLaki, 0), Colors.green), _sdLine( _getSDRefBBTB_Gabungan(isLaki, -2), Colors.red), _sdLine( _getSDRefBBTB_Gabungan(isLaki, -3), Colors.black), LineChartBarData( spots: historySpots, isCurved: true, color: isLaki ? Colors.blue.shade700 : Colors.pink.shade600, barWidth: 4, dotData: FlDotData( show: true, getDotPainter: (spot, p, bar, i) => FlDotCirclePainter( radius: 6, color: Colors.white, strokeWidth: 3, strokeColor: isLaki ? Colors.blue.shade700 : Colors.pink.shade600))), ], ), ), ), ), ], ), ), // Kotak Status Gizi di pinggir kiri 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, "Kurus"), _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 Gizi: ", style: TextStyle(fontWeight: FontWeight.normal)), TextSpan( text: "${analisis["status"]}", style: const TextStyle( fontWeight: FontWeight.bold)), ], ), ), const SizedBox(height: 4), Text( "Terakhir: ${bb}kg / ${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 _getSDRefBBTB_Gabungan(bool isLaki, int type) { if (isLaki) { switch (type) { case 3: return const [ FlSpot(45, 3.0), FlSpot(50, 4.3), FlSpot(60, 7.3), FlSpot(70, 10.1), FlSpot(80, 12.8), FlSpot(90, 15.6), FlSpot(100, 19.1), FlSpot(110, 23.1), FlSpot(120, 27.7) ]; case 2: return const [ FlSpot(45, 2.8), FlSpot(50, 4.0), FlSpot(60, 6.7), FlSpot(70, 9.3), FlSpot(80, 11.8), FlSpot(90, 14.3), FlSpot(100, 17.5), FlSpot(110, 21.1), FlSpot(120, 25.2) ]; case 0: return const [ FlSpot(45, 2.4), FlSpot(50, 3.4), FlSpot(60, 5.7), FlSpot(70, 7.9), FlSpot(80, 10.0), FlSpot(90, 12.2), FlSpot(100, 14.8), FlSpot(110, 17.8), FlSpot(120, 21.1) ]; case -2: return const [ FlSpot(45, 2.1), FlSpot(50, 2.9), FlSpot(60, 4.9), FlSpot(70, 6.8), FlSpot(80, 8.6), FlSpot(90, 10.5), FlSpot(100, 12.8), FlSpot(110, 15.3), FlSpot(120, 18.0) ]; case -3: return const [ FlSpot(45, 1.9), FlSpot(50, 2.7), FlSpot(60, 4.5), FlSpot(70, 6.3), FlSpot(80, 7.9), FlSpot(90, 9.7), FlSpot(100, 11.8), FlSpot(110, 14.1), FlSpot(120, 16.5) ]; } } else { switch (type) { case 3: return const [ FlSpot(45, 2.9), FlSpot(50, 4.2), FlSpot(60, 7.0), FlSpot(70, 9.8), FlSpot(80, 12.6), FlSpot(90, 15.7), FlSpot(100, 19.5), FlSpot(110, 23.9), FlSpot(120, 29.1) ]; case 2: return const [ FlSpot(45, 2.7), FlSpot(50, 3.8), FlSpot(60, 6.4), FlSpot(70, 9.0), FlSpot(80, 11.6), FlSpot(90, 14.4), FlSpot(100, 17.8), FlSpot(110, 21.7), FlSpot(120, 26.3) ]; case 0: return const [ FlSpot(45, 2.3), FlSpot(50, 3.3), FlSpot(60, 5.4), FlSpot(70, 7.6), FlSpot(80, 9.8), FlSpot(90, 12.2), FlSpot(100, 15.0), FlSpot(110, 18.3), FlSpot(120, 21.9) ]; case -2: return const [ FlSpot(45, 2.0), FlSpot(50, 2.8), FlSpot(60, 4.6), FlSpot(70, 6.5), FlSpot(80, 8.3), FlSpot(90, 10.3), FlSpot(100, 12.8), FlSpot(110, 15.4), FlSpot(120, 18.4) ]; case -3: return const [ FlSpot(45, 1.8), FlSpot(50, 2.6), FlSpot(60, 4.3), FlSpot(70, 6.0), FlSpot(80, 7.7), FlSpot(90, 9.5), FlSpot(100, 11.7), FlSpot(110, 14.1), FlSpot(120, 16.8) ]; } } return []; } }