MIF_E31230549/lib/ibu/crud_grafik/grafik_anak_tb_usia.dart

710 lines
26 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 GrafikAnakTBUPage extends StatefulWidget {
const GrafikAnakTBUPage({super.key});
@override
State<GrafikAnakTBUPage> createState() => _GrafikAnakTBUPageState();
}
class _GrafikAnakTBUPageState extends State<GrafikAnakTBUPage> {
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_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<FlSpot> spots = [];
List<Map<String, dynamic>> 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<FlSpot> historySpots;
final List<Map<String, dynamic>> 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<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> 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<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:
"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<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> _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 [];
}
}