MIF_E31230549/lib/ibu/crud_grafik/grafik_anak_bb_tb.dart

685 lines
25 KiB
Dart

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<GrafikAnakBBTBPage> createState() => _GrafikAnakBBTBPageState();
}
class _GrafikAnakBBTBPageState extends State<GrafikAnakBBTBPage> {
List anakList = [];
bool isLoading = true;
@override
void initState() {
super.initState();
getData();
}
Future<void> 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<FlSpot> spots = [];
List<Map<String, dynamic>> 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<FlSpot> historySpots;
final List<Map<String, dynamic>> 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<FlSpot> 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<String, dynamic> 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<LineBarSpot> 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<FlSpot> spots, Color color) {
return LineChartBarData(
spots: spots,
color: color.withOpacity(0.35),
barWidth: 1.5,
dotData: const FlDotData(show: false),
isCurved: true);
}
List<FlSpot> _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 [];
}
}