MIF_E31230549/lib/ibu/crud_grafik/grafik_anak_bb_usia.dart

726 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 GrafikAnakBBUsiaPage extends StatefulWidget {
const GrafikAnakBBUsiaPage({super.key});
@override
State<GrafikAnakBBUsiaPage> createState() => _GrafikAnakIbuPageState();
}
class _GrafikAnakIbuPageState extends State<GrafikAnakBBUsiaPage> {
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_balita.php"),
body: {"user_id": userId},
);
var jsonData = json.decode(response.body);
if (jsonData["success"] &&
jsonData["data"] != null &&
jsonData["data"].isNotEmpty) {
Map<String, dynamic> 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<Map<String, dynamic>> detailSpots = [];
List<FlSpot> 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<FlSpot> historySpots;
final List<Map<String, dynamic>> 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<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> 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<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": "${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<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> _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 [];
}
}