MIF_E31230549/lib/ibu/crud_grafik/grafik_balita_imt.dart

517 lines
18 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';
// Sesuaikan path import ini dengan project Anda
import '../../layout/main_layout.dart';
import '../ibu_drawer.dart';
// Import Dashboard Ibu agar navigasi PopScope berfungsi
import '../dashboard_ibu.dart';
class GrafikAnakIMTPage extends StatefulWidget {
const GrafikAnakIMTPage({super.key});
@override
State<GrafikAnakIMTPage> createState() => _GrafikAnakIMTPageState();
}
class _GrafikAnakIMTPageState extends State<GrafikAnakIMTPage> {
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") ?? "";
try {
var response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/grafik_balita/get_grafik_imt.php"),
body: {"user_id": userId},
);
var jsonData = json.decode(response.body);
if (jsonData["success"]) {
setState(() {
anakList = jsonData["data"];
isLoading = false;
});
}
} catch (e) {
if (mounted) setState(() => isLoading = false);
debugPrint("Error: $e");
}
}
int hitungUmurBulan(String tglLahir, String tglPeriksa) {
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;
}
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) {
days += DateTime(now.year, now.month, 0).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: Padding(
padding: const EdgeInsets.all(10),
child: isLoading
? const Center(child: CircularProgressIndicator())
: anakList.isEmpty
? const Center(child: Text("Tidak ada data IMT."))
: ListView.builder(
itemCount: anakList.length,
itemBuilder: (context, index) {
var anak = anakList[index];
List<FlSpot> spots = [];
List<Map<String, dynamic>> riwayatDetails = [];
double lastIMT = 0;
double lastBB = 0;
double lastTB = 0;
int lastBulan = 0;
String lastTgl = "";
for (var p in anak["riwayat_imt"]) {
double imtVal =
double.tryParse(p["imt"].toString()) ?? 0.0;
if (imtVal > 0) {
int bulan = hitungUmurBulan(anak["tanggal_lahir"],
p["tanggal_pemeriksaan"]);
String usiaStr = formatUsiaDetail(
anak["tanggal_lahir"],
p["tanggal_pemeriksaan"]);
spots.add(FlSpot(bulan.toDouble(), imtVal));
riwayatDetails.add({
"x": bulan.toDouble(),
"y": imtVal,
"bb": p["bb"],
"tb": p["tb"],
"usia_lengkap": usiaStr,
});
lastIMT = imtVal;
lastBB = p["bb"];
lastTB = p["tb"];
lastBulan = bulan;
lastTgl = p["tanggal_pemeriksaan"];
}
}
return GrafikCardKIA_IMT(
nama: anak["nama"],
jk: anak["jenis_kelamin"],
umurBulan: lastBulan,
imt: lastIMT,
bb: lastBB,
tb: lastTB,
detailUsiaTerakhir: lastTgl.isNotEmpty
? formatUsiaDetail(anak["tanggal_lahir"], lastTgl)
: "-",
historySpots: spots,
historyDetails: riwayatDetails,
);
},
),
),
),
);
}
}
class GrafikCardKIA_IMT extends StatelessWidget {
final String nama, jk, detailUsiaTerakhir;
final int umurBulan;
final double imt, bb, tb;
final List<FlSpot> historySpots;
final List<Map<String, dynamic>> historyDetails;
const GrafikCardKIA_IMT({
super.key,
required this.nama,
required this.jk,
required this.umurBulan,
required this.imt,
required this.bb,
required this.tb,
required this.detailUsiaTerakhir,
required this.historySpots,
required this.historyDetails,
});
double getSDValueAtAge(List<FlSpot> 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<String, dynamic> analisisStatusIMT(
bool isLaki, int umur, double nilaiIMT) {
if (umur > 60 || nilaiIMT <= 0)
return {"status": "Data tidak valid", "warna": Colors.grey};
double sd3 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, 3), umur);
double sd2 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, 2), umur);
double sd1 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, 1), umur);
double sd0 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, 0), umur);
double sdM2 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, -2), umur);
double sdM3 = getSDValueAtAge(_getSDRefIMT_Gabungan(isLaki, -3), umur);
if (nilaiIMT < sdM3)
return {"status": "Gizi Buruk (Sangat Kurus)", "warna": Colors.black};
if (nilaiIMT < sdM2)
return {"status": "Gizi Kurang (Kurus)", "warna": Colors.red};
if (nilaiIMT > sd3) return {"status": "Obesitas", "warna": Colors.purple};
if (nilaiIMT > sd2)
return {"status": "Berat Badan Lebih", "warna": Colors.orange.shade900};
if (nilaiIMT > sd1)
return {"status": "Risiko Berat Badan Lebih", "warna": Colors.orange};
if (nilaiIMT >= sd0)
return {"status": "Normal (Di atas Median)", "warna": Colors.green};
return {
"status": "Normal (Di bawah Median)",
"warna": Colors.green.shade700
};
}
@override
Widget build(BuildContext context) {
final bool isLaki = jk == "L";
final Color accentColor =
isLaki ? Colors.blue.shade700 : const Color(0xFFE91E63);
final analisis = analisisStatusIMT(isLaki, umurBulan, imt);
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
margin: const EdgeInsets.only(bottom: 25),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: accentColor,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(15))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(nama,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15)),
Text(isLaki ? "Laki-laki" : "Perempuan",
style:
GoogleFonts.poppins(color: Colors.white, fontSize: 11)),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 25, 25, 10),
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) {
var detail = historyDetails.firstWhere(
(e) => e["x"] == spot.x && e["y"] == spot.y,
orElse: () => {});
return LineTooltipItem(
"IMT: ${spot.y.toStringAsFixed(1)}\nBB: ${detail["bb"]}kg, TB: ${detail["tb"]}cm\n${detail["usia_lengkap"]}",
GoogleFonts.poppins(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold),
);
}).toList();
},
),
),
minX: 0,
maxX: 60,
minY: 10,
maxY: 22,
titlesData: FlTitlesData(
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (val, _) => Text(
val.toInt().toString(),
style: GoogleFonts.poppins(
fontSize: 9, color: Colors.black54)))),
leftTitles: AxisTitles(
axisNameWidget: Text("IMT",
style: GoogleFonts.poppins(
fontSize: 10, fontWeight: FontWeight.bold)),
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (val, _) => Text(
val.toInt().toString(),
style: GoogleFonts.poppins(
fontSize: 9, color: Colors.black54)))),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 12,
getTitlesWidget: (val, _) {
if (val == 0)
return Text("Lahir",
style: GoogleFonts.poppins(fontSize: 9));
return Text("${(val / 12).toInt()} Thn",
style: GoogleFonts.poppins(fontSize: 9));
}),
),
),
gridData: FlGridData(
show: true, horizontalInterval: 1, verticalInterval: 6),
borderData: FlBorderData(
show: true, border: Border.all(color: Colors.black12)),
lineBarsData: [
_sdLine(_getSDRefIMT_Gabungan(isLaki, 3), Colors.black),
_sdLine(_getSDRefIMT_Gabungan(isLaki, 2), Colors.red),
_sdLine(_getSDRefIMT_Gabungan(isLaki, 1), Colors.orange),
_sdLine(_getSDRefIMT_Gabungan(isLaki, 0), Colors.green),
_sdLine(_getSDRefIMT_Gabungan(isLaki, -2), Colors.red),
_sdLine(_getSDRefIMT_Gabungan(isLaki, -3), Colors.black),
LineChartBarData(
spots: historySpots,
isCurved: true,
color: accentColor,
barWidth: 3,
dotData: FlDotData(
show: true,
getDotPainter: (spot, p, bar, i) =>
FlDotCirclePainter(
radius: 4,
color: Colors.white,
strokeWidth: 2,
strokeColor: accentColor)),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade200)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Keterangan Grafik:",
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold, fontSize: 12)),
const SizedBox(height: 8),
_legendItem(Colors.green, "Hijau", "Median (Ideal)"),
_legendItem(
Colors.orange, "Orange", "Ambang Normal Atas (+1 SD)"),
_legendItem(Colors.red, "Merah", "Batas Kurus / Gemuk"),
_legendItem(Colors.black, "Hitam", "Kritis"),
const Divider(height: 20),
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: analisis["warna"],
shape: BoxShape.circle)),
const SizedBox(width: 8),
Expanded(
child: Text("Status: ${analisis["status"]}",
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.bold,
color: analisis["warna"]))),
],
),
const SizedBox(height: 5),
Text(
"Terakhir: ${imt.toStringAsFixed(1)} kg/m² | Usia: $detailUsiaTerakhir",
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
color: accentColor)),
],
),
),
)
],
),
);
}
Widget _legendItem(Color color, String label, String desc) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(children: [
Container(width: 10, height: 1.5, color: color),
const SizedBox(width: 8),
Text("$label: $desc", style: GoogleFonts.poppins(fontSize: 10))
]));
}
LineChartBarData _sdLine(List<FlSpot> spots, Color color) => LineChartBarData(
spots: spots,
color: color.withOpacity(0.4),
barWidth: 1.2,
dotData: const FlDotData(show: false),
isCurved: true);
List<FlSpot> _getSDRefIMT_Gabungan(bool isLaki, int type) {
if (isLaki) {
if (type == 3)
return const [
FlSpot(0, 16.1),
FlSpot(6, 19.3),
FlSpot(12, 19.9),
FlSpot(60, 19.6)
];
if (type == 2)
return const [
FlSpot(0, 15.3),
FlSpot(6, 18.2),
FlSpot(12, 18.9),
FlSpot(60, 18.8)
];
if (type == 1)
return const [
FlSpot(0, 14.4),
FlSpot(6, 17.2),
FlSpot(12, 18.0),
FlSpot(60, 18.0)
];
if (type == 0)
return const [
FlSpot(0, 13.4),
FlSpot(6, 16.3),
FlSpot(12, 17.1),
FlSpot(60, 17.0)
];
if (type == -2)
return const [
FlSpot(0, 11.5),
FlSpot(6, 14.3),
FlSpot(12, 15.1),
FlSpot(60, 15.1)
];
if (type == -3)
return const [
FlSpot(0, 10.7),
FlSpot(6, 13.2),
FlSpot(12, 14.1),
FlSpot(60, 14.1)
];
} else {
if (type == 3)
return const [
FlSpot(0, 16.0),
FlSpot(6, 19.1),
FlSpot(12, 19.7),
FlSpot(60, 21.0)
];
if (type == 2)
return const [
FlSpot(0, 15.1),
FlSpot(6, 17.9),
FlSpot(12, 18.6),
FlSpot(60, 19.9)
];
if (type == 1)
return const [
FlSpot(0, 14.2),
FlSpot(6, 16.9),
FlSpot(12, 17.6),
FlSpot(60, 18.9)
];
if (type == 0)
return const [
FlSpot(0, 13.2),
FlSpot(6, 15.8),
FlSpot(12, 16.5),
FlSpot(60, 17.8)
];
if (type == -2)
return const [
FlSpot(0, 11.2),
FlSpot(6, 13.8),
FlSpot(12, 14.4),
FlSpot(60, 15.6)
];
if (type == -3)
return const [
FlSpot(0, 10.4),
FlSpot(6, 12.8),
FlSpot(12, 13.3),
FlSpot(60, 14.6)
];
}
return [];
}
}