435 lines
14 KiB
Dart
435 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../layout/main_layout.dart';
|
|
import 'bidan_drawer.dart';
|
|
import '../pages/login_page.dart';
|
|
import '../bidan/data_gizi_balita.dart';
|
|
import '../bidan/laporan.dart';
|
|
|
|
const String baseUrl = "http://ta.myhost.id/E31230549/mposyandu_api";
|
|
|
|
class DashboardBidanPage extends StatefulWidget {
|
|
const DashboardBidanPage({super.key});
|
|
|
|
@override
|
|
State<DashboardBidanPage> createState() => _DashboardBidanPageState();
|
|
}
|
|
|
|
class _DashboardBidanPageState extends State<DashboardBidanPage> {
|
|
int jumlahIbuHamil = 0;
|
|
int jumlahBalita = 0;
|
|
|
|
// Variabel penampung data grafik Ibu Hamil
|
|
double bumilKek = 0;
|
|
double bumilNormal = 0;
|
|
|
|
String namaUser = "";
|
|
|
|
final List<String> listBulan = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'Mei',
|
|
'Jun',
|
|
'Jul',
|
|
'Agu',
|
|
'Sep',
|
|
'Okt',
|
|
'Nov',
|
|
'Des'
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_checkLogin();
|
|
getDashboard();
|
|
}
|
|
|
|
Future<void> _checkLogin() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final isLogin = prefs.getBool('isLogin') ?? false;
|
|
|
|
if (!isLogin) {
|
|
if (!mounted) return;
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const LoginPage(fromGuard: true)),
|
|
(route) => false,
|
|
);
|
|
} else {
|
|
setState(() {
|
|
namaUser = prefs.getString('nama') ?? "Bidan";
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> getDashboard() async {
|
|
try {
|
|
final response =
|
|
await http.get(Uri.parse("$baseUrl/dashboard_bidan.php"));
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (data["success"] == true) {
|
|
setState(() {
|
|
jumlahIbuHamil = int.parse(data["ibu_hamil"].toString());
|
|
jumlahBalita = int.parse(data["balita"].toString());
|
|
|
|
// Menangkap data kondisi pemeriksaan LILA terbaru dari JSON
|
|
bumilKek = double.parse((data["bumil_kek"] ?? 0).toString());
|
|
bumilNormal = double.parse((data["bumil_normal"] ?? 0).toString());
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("ERROR DASHBOARD: $e");
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk mendeteksi klik pada diagram batang
|
|
void _handleChartTap(TapUpDetails details, Size size) {
|
|
const double paddingLeft = 30;
|
|
const double paddingBottom = 30;
|
|
final double chartWidth = size.width - paddingLeft;
|
|
final double chartHeight = size.height - paddingBottom;
|
|
|
|
double maxData = max(10, max(bumilNormal, bumilKek) + 5);
|
|
double groupWidth = chartWidth / listBulan.length;
|
|
double barWidth = groupWidth * 0.3;
|
|
|
|
int currentMonthIndex = DateTime.now().month - 1;
|
|
|
|
// Hitung posisi koordinat X pusat untuk bulan saat ini
|
|
double xCenter =
|
|
paddingLeft + (currentMonthIndex * groupWidth) + (groupWidth / 2);
|
|
|
|
// Hitung area Rect untuk Batang Normal
|
|
double barHeightNormal = (bumilNormal / maxData) * chartHeight;
|
|
Rect rectNormal = Rect.fromLTWH(xCenter - barWidth,
|
|
chartHeight - barHeightNormal, barWidth, barHeightNormal);
|
|
|
|
// Hitung area Rect untuk Batang KEK
|
|
double barHeightKek = (bumilKek / maxData) * chartHeight;
|
|
Rect rectKek = Rect.fromLTWH(
|
|
xCenter, chartHeight - barHeightKek, barWidth, barHeightKek);
|
|
|
|
// Ambil posisi lokal ketukan jari user
|
|
Offset tapPosition = details.localPosition;
|
|
|
|
// Cek apakah ketukan berada di dalam salah satu area batang
|
|
if (rectNormal.contains(tapPosition)) {
|
|
_showDetailDialog("Bumil Normal", bumilNormal.round());
|
|
} else if (rectKek.contains(tapPosition)) {
|
|
_showDetailDialog("Berisiko KEK", bumilKek.round());
|
|
}
|
|
}
|
|
|
|
// Fungsi memunculkan pop-up angka pasti
|
|
void _showDetailDialog(String kategori, int jumlah) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
title: Text(kategori,
|
|
style: GoogleFonts.poppins(fontWeight: FontWeight.bold)),
|
|
content: Text(
|
|
"Jumlah saat ini: $jumlah Ibu Hamil",
|
|
style: GoogleFonts.poppins(fontSize: 15),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("OK",
|
|
style:
|
|
TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
await SystemNavigator.pop();
|
|
},
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme),
|
|
dividerColor: Colors.transparent,
|
|
),
|
|
child: MainLayout(
|
|
title: "",
|
|
drawer: const BidanDrawer(),
|
|
body: _buildBody(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
return SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
RichText(
|
|
text: TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: 'Selamat Datang Bidan $namaUser\n',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
TextSpan(
|
|
text:
|
|
'Melayani dengan sepenuh hati untuk generasi tetap sehat ',
|
|
style:
|
|
GoogleFonts.poppins(fontSize: 13, color: Colors.black),
|
|
),
|
|
const WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Icon(Icons.favorite, color: Colors.blue, size: 18),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Center(
|
|
child: Image.asset(
|
|
'assets/images/logoo.webp',
|
|
width: 300,
|
|
height: 180,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
const SizedBox(height: 25),
|
|
Row(
|
|
children: [
|
|
_infoBox("$jumlahIbuHamil", "Ibu Hamil Aktif", Colors.pink),
|
|
const SizedBox(width: 10),
|
|
_infoBox("$jumlahBalita", "Balita Terdaftar", Colors.amber),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
_actionButton('Lihat Data Gizi', () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DataGiziBalitaPage()));
|
|
}),
|
|
const SizedBox(width: 10),
|
|
_actionButton('Lihat Laporan', () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DataLaporanPage()));
|
|
}),
|
|
],
|
|
),
|
|
const SizedBox(height: 25),
|
|
Text(
|
|
'Statistik Risiko KEK Ibu Hamil Bulan Ini',
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Container(
|
|
padding: const EdgeInsets.fromLTRB(10, 25, 15, 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
spreadRadius: 2,
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Menggunakan LayoutBuilder agar size CustomPaint sinkron dengan pendeteksi klik
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final size = Size(constraints.maxWidth, 250);
|
|
return GestureDetector(
|
|
onTapUp: (details) => _handleChartTap(details, size),
|
|
child: SizedBox(
|
|
height: size.height,
|
|
width: size.width,
|
|
child: CustomPaint(
|
|
painter: BarChartPainter(
|
|
normal: bumilNormal,
|
|
kek: bumilKek,
|
|
months: listBulan,
|
|
currentMonthIndex: DateTime.now().month - 1,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_legendItem(Colors.teal, "Bumil Normal"),
|
|
const SizedBox(width: 25),
|
|
_legendItem(Colors.pink.shade400, "Berisiko KEK"),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _actionButton(String title, VoidCallback onPressed) {
|
|
return Expanded(
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
onPressed: onPressed,
|
|
child: Text(title, style: const TextStyle(color: Colors.white)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _legendItem(Color color, String text) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
width: 14,
|
|
height: 14,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(text,
|
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _infoBox(String value, String label, Color color) {
|
|
return Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: color, borderRadius: BorderRadius.circular(12)),
|
|
child: Column(
|
|
children: [
|
|
Text(value,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 5),
|
|
Text(label,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class BarChartPainter extends CustomPainter {
|
|
final double normal;
|
|
final double kek;
|
|
final List<String> months;
|
|
final int currentMonthIndex;
|
|
|
|
BarChartPainter({
|
|
required this.normal,
|
|
required this.kek,
|
|
required this.months,
|
|
required this.currentMonthIndex,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paintGrid = Paint()
|
|
..color = Colors.grey.shade300
|
|
..strokeWidth = 1;
|
|
const double paddingLeft = 30;
|
|
const double paddingBottom = 30;
|
|
final double chartWidth = size.width - paddingLeft;
|
|
final double chartHeight = size.height - paddingBottom;
|
|
|
|
double maxData = max(10, max(normal, kek) + 5);
|
|
|
|
int segments = 5;
|
|
for (int i = 0; i <= segments; i++) {
|
|
double y = chartHeight - (i * chartHeight / segments);
|
|
canvas.drawLine(Offset(paddingLeft, y), Offset(size.width, y), paintGrid);
|
|
_drawText(canvas, Offset(5, y - 7),
|
|
(maxData / segments * i).round().toString(), 10, Colors.grey);
|
|
}
|
|
|
|
double groupWidth = chartWidth / months.length;
|
|
double barWidth = groupWidth * 0.3;
|
|
|
|
for (int i = 0; i < months.length; i++) {
|
|
double xCenter = paddingLeft + (i * groupWidth) + (groupWidth / 2);
|
|
_drawText(canvas, Offset(xCenter - 10, chartHeight + 10), months[i], 9,
|
|
Colors.black);
|
|
|
|
if (i == currentMonthIndex) {
|
|
double barHeightNormal = (normal / maxData) * chartHeight;
|
|
canvas.drawRect(
|
|
Rect.fromLTWH(xCenter - barWidth, chartHeight - barHeightNormal,
|
|
barWidth, barHeightNormal),
|
|
Paint()..color = Colors.teal,
|
|
);
|
|
|
|
double barHeightKek = (kek / maxData) * chartHeight;
|
|
canvas.drawRect(
|
|
Rect.fromLTWH(
|
|
xCenter, chartHeight - barHeightKek, barWidth, barHeightKek),
|
|
Paint()..color = Colors.pink.shade400,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _drawText(
|
|
Canvas canvas, Offset offset, String text, double fontSize, Color color) {
|
|
final textPainter = TextPainter(
|
|
text: TextSpan(
|
|
text: text,
|
|
style: TextStyle(
|
|
color: color, fontSize: fontSize, fontWeight: FontWeight.bold)),
|
|
textDirection: TextDirection.ltr,
|
|
);
|
|
textPainter.layout();
|
|
textPainter.paint(canvas, offset);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
}
|