MIF_E31230549/lib/bidan/laporan.dart

675 lines
26 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../layout/main_layout.dart';
import 'bidan_drawer.dart';
import '../bidan/dashboard_bidan.dart';
class DataLaporanPage extends StatefulWidget {
const DataLaporanPage({super.key});
@override
State<DataLaporanPage> createState() => _DataLaporanPageStatus();
}
class _DataLaporanPageStatus extends State<DataLaporanPage> {
int _currentPage = 0;
final int _rowsPerPage = 10;
String _searchQuery = "";
List<dynamic> _allData = [];
final List<String> _namaBulan = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
Future<Map<String, dynamic>> fetchPemeriksaanBalita() async {
try {
final response = await http.get(Uri.parse(
'http://ta.myhost.id/E31230549/mposyandu_api/laporan/get_laporan.php'));
if (response.statusCode == 200) {
Map<String, dynamic> data = json.decode(response.body);
_allData = data['data'] ?? [];
return data;
} else {
throw Exception('Gagal mengambil data dari database');
}
} catch (e) {
throw Exception('Kesalahan koneksi: $e');
}
}
void _showMonthSelectionDialog() async {
List<int> selectedMonths = [];
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
title: Text("Pilih Bulan",
style: GoogleFonts.poppins(
fontSize: 16, fontWeight: FontWeight.bold)),
content: SizedBox(
width: 300,
height: 300,
child: GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3,
),
itemCount: _namaBulan.length,
itemBuilder: (context, index) {
return CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(_namaBulan[index],
style: GoogleFonts.poppins(fontSize: 12)),
value: selectedMonths.contains(index + 1),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (bool? value) {
setDialogState(() {
if (value == true) {
selectedMonths.add(index + 1);
} else {
selectedMonths.remove(index + 1);
}
});
},
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text("Batal",
style: GoogleFonts.poppins(color: Colors.grey)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
onPressed: () {
Navigator.pop(context);
if (selectedMonths.isNotEmpty) {
_generateFilteredPdf(selectedMonths);
}
},
child: Text("Cetak",
style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
},
);
},
);
}
// ================= GENERATE PDF DENGAN REKAP GIZI BARU =================
Future<void> _generateFilteredPdf(List<int> selectedMonths) async {
List<dynamic> filteredData = _allData.where((item) {
final tglString = item['tgl_periksa']?.toString() ?? '';
if (tglString.isEmpty || tglString == '-') return false;
try {
final parts = tglString.split('-');
if (parts.length != 3) return false;
final bulan = int.parse(parts[1]);
return selectedMonths.contains(bulan);
} catch (e) {
return false;
}
}).toList();
if (filteredData.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Tidak ada data pada bulan yang dipilih"),
backgroundColor: Colors.orange),
);
return;
}
// Variabel Rekapitulasi (Sudah digunakan di bawah agar garis kuning hilang)
int bbuGiziBuruk = 0,
bbuGiziKurang = 0,
bbuGiziBaik = 0,
bbuRisikoLebih = 0;
int tbuSangatPendek = 0, tbuPendek = 0, tbuNormal = 0, tbuTinggi = 0;
int bbtbGiziBuruk = 0,
bbtbGiziKurang = 0,
bbtbGiziBaik = 0,
bbtbRisikoLebih = 0,
bbtbObesitas = 0;
for (var item in filteredData) {
String bbu = (item['status_bbu'] ?? '').toString().trim().toLowerCase();
String tbu = (item['status_tbu'] ?? '').toString().trim().toLowerCase();
String bbtb = (item['status_bbtb'] ?? '').toString().trim().toLowerCase();
if (bbu.contains('buruk'))
bbuGiziBuruk++;
else if (bbu.contains('kurang'))
bbuGiziKurang++;
else if (bbu.contains('baik') || bbu.contains('normal'))
bbuGiziBaik++;
else if (bbu.contains('lebih')) bbuRisikoLebih++;
if (tbu.contains('sangat pendek'))
tbuSangatPendek++;
else if (tbu.contains('pendek') || tbu.contains('stunting'))
tbuPendek++;
else if (tbu.contains('normal'))
tbuNormal++;
else if (tbu.contains('tinggi')) tbuTinggi++;
if (bbtb.contains('buruk') || bbtb.contains('wasting'))
bbtbGiziBuruk++;
else if (bbtb.contains('kurang'))
bbtbGiziKurang++;
else if (bbtb.contains('baik') || bbtb.contains('normal'))
bbtbGiziBaik++;
else if (bbtb.contains('risiko') || bbtb.contains('overweight'))
bbtbRisikoLebih++;
else if (bbtb.contains('obesitas')) bbtbObesitas++;
}
final pdf = pw.Document();
pw.MemoryImage? logoImage;
try {
final ByteData assetImage =
await rootBundle.load('assets/images/logo sumberasih.png');
logoImage = pw.MemoryImage(assetImage.buffer.asUint8List());
} catch (e) {
debugPrint("Logo gagal dimuat");
}
const int pdfItemsPerPage = 12;
final int totalPages = (filteredData.length / pdfItemsPerPage).ceil();
final currentYear = DateTime.now().year;
for (int i = 0; i < totalPages; i++) {
final int start = i * pdfItemsPerPage;
final int end = (start + pdfItemsPerPage < filteredData.length)
? start + pdfItemsPerPage
: filteredData.length;
final List<dynamic> pageData = filteredData.sublist(start, end);
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4.landscape,
margin: const pw.EdgeInsets.all(15),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// ================= HEADER =================
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
if (logoImage != null)
pw.Container(
width: 50, height: 50, child: pw.Image(logoImage)),
pw.SizedBox(width: 15),
pw.Expanded(
child: pw.Column(
children: [
pw.Text("LAPORAN KEGIATAN KUNJUNGAN BALITA",
style: pw.TextStyle(
fontSize: 16,
fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 2),
pw.Text("Posyandu Desa Mentor",
style: const pw.TextStyle(fontSize: 11)),
pw.Text("Kecamatan Sumberasih, Kabupaten Probolinggo",
style: const pw.TextStyle(fontSize: 11)),
pw.SizedBox(height: 2),
pw.Text(
"Bulan: ${selectedMonths.map((m) => _namaBulan[m - 1]).join(', ')} $currentYear",
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold)),
],
),
),
],
),
pw.SizedBox(height: 5),
pw.Divider(thickness: 1.5),
pw.SizedBox(height: 5),
// ================= TABEL DATA =================
pw.TableHelper.fromTextArray(
border:
pw.TableBorder.all(color: PdfColors.grey700, width: 0.5),
cellAlignment: pw.Alignment.centerLeft,
headerAlignment: pw.Alignment.center,
headers: [
'No',
'NIK',
'Nama Balita',
'Orang Tua',
'TTL',
'Umur',
'JK',
'Anak',
'Alamat',
'Tgl Periksa',
'BB',
'TB',
'LK',
'Imunisasi',
'BB/U',
'TB/U',
'BB/TB',
'Hadir'
],
data: List<List<dynamic>>.generate(pageData.length, (index) {
final data = pageData[index];
return [
(start + index + 1).toString(),
data['nik_balita'] ?? "-",
data['nama'] ?? "-",
data['nama_orang_tua'] ?? "-",
"${data['tempat_lahir'] ?? '-'}, ${data['tgl_lahir'] ?? '-'}",
"${data['umur'] ?? '-'} Bln",
data['jenis_kelamin'] ?? "-",
data['anak_ke']?.toString() ?? "-",
data['alamat_lengkap'] ?? "-",
data['tgl_periksa'] ?? "-",
data['berat_badan']?.toString() ?? "-",
data['tinggi_badan']?.toString() ?? "-",
data['lingkar_kepala']?.toString() ?? "-",
data['pemberian_imunisasi'] ?? "-",
data['status_bbu'] ?? "-",
data['status_tbu'] ?? "-",
data['status_bbtb'] ?? "-",
data['kehadiran_posyandu'] ?? "-",
];
}),
headerStyle: pw.TextStyle(
fontSize: 7,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white),
headerDecoration:
const pw.BoxDecoration(color: PdfColors.blue700),
cellStyle: const pw.TextStyle(fontSize: 6.5),
cellHeight: 24,
columnWidths: {
0: const pw.FixedColumnWidth(20),
1: const pw.FixedColumnWidth(60),
2: const pw.FixedColumnWidth(65),
3: const pw.FixedColumnWidth(70),
4: const pw.FixedColumnWidth(75),
5: const pw.FixedColumnWidth(30),
6: const pw.FixedColumnWidth(20),
7: const pw.FixedColumnWidth(25),
8: const pw.FixedColumnWidth(90),
9: const pw.FixedColumnWidth(45),
10: const pw.FixedColumnWidth(25),
11: const pw.FixedColumnWidth(25),
12: const pw.FixedColumnWidth(25),
13: const pw.FixedColumnWidth(50),
14: const pw.FixedColumnWidth(50),
15: const pw.FixedColumnWidth(50),
16: const pw.FixedColumnWidth(50),
17: const pw.FixedColumnWidth(40),
},
),
// ================= FOOTER REKAPITULASI (HALAMAN TERAKHIR) =================
if (i == totalPages - 1) ...[
pw.SizedBox(height: 12),
pw.Row(
// PERBAIKAN: Menggunakan pw.MainAxisAlignment.spaceBetween agar tidak error garis merah
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text("Total Data: ${filteredData.length} Anak",
style: pw.TextStyle(
fontSize: 9, fontWeight: pw.FontWeight.bold)),
pw.Container(
width: 480,
padding: const pw.EdgeInsets.all(6),
decoration: pw.BoxDecoration(
border: pw.Border.all(
color: PdfColors.black, width: 0.5)),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text("Keterangan Rekapitulasi Status Gizi",
style: pw.TextStyle(
fontWeight: pw.FontWeight.bold,
fontSize: 9)),
pw.SizedBox(height: 4),
pw.Row(
mainAxisAlignment:
pw.MainAxisAlignment.spaceBetween,
children: [
pw.Column(
crossAxisAlignment:
pw.CrossAxisAlignment.start,
children: [
pw.Text("[BB/U] Berat Badan / Umur:",
style: pw.TextStyle(
fontSize: 7,
fontWeight: pw.FontWeight.bold)),
pw.Text(
"- Gizi Baik: $bbuGiziBaik\n- Gizi Kurang: $bbuGiziKurang\n- Gizi Buruk: $bbuGiziBuruk\n- Risiko Lebih: $bbuRisikoLebih",
style: const pw.TextStyle(fontSize: 7)),
],
),
pw.Column(
crossAxisAlignment:
pw.CrossAxisAlignment.start,
children: [
pw.Text("[TB/U] Tinggi Badan / Umur:",
style: pw.TextStyle(
fontSize: 7,
fontWeight: pw.FontWeight.bold)),
pw.Text(
"- Normal: $tbuNormal\n- Pendek: $tbuPendek\n- Sangat Pendek: $tbuSangatPendek\n- Tinggi: $tbuTinggi",
style: const pw.TextStyle(fontSize: 7)),
],
),
pw.Column(
crossAxisAlignment:
pw.CrossAxisAlignment.start,
children: [
pw.Text("[BB/TB] Berat Badan / Tinggi:",
style: pw.TextStyle(
fontSize: 7,
fontWeight: pw.FontWeight.bold)),
pw.Text(
"- Normal: $bbtbGiziBaik\n- Gizi Kurang: $bbtbGiziKurang\n- Gizi Buruk: $bbtbGiziBuruk\n- Overweight: $bbtbRisikoLebih\n- Obesitas: $bbtbObesitas",
style: const pw.TextStyle(fontSize: 7)),
],
),
],
),
],
),
),
],
),
],
],
);
},
),
);
}
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => pdf.save());
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const DashboardBidanPage()),
(route) => false);
},
child: MainLayout(
title: "",
drawer: const BidanDrawer(),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text("Laporan Pemeriksaan Balita",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.w600)),
),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerRight,
child: SizedBox(
height: 35,
child: OutlinedButton.icon(
onPressed: () => _allData.isNotEmpty
? _showMonthSelectionDialog()
: null,
icon: const Icon(Icons.print, size: 16, color: Colors.blue),
label: Text("Cetak PDF",
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.blue,
fontWeight: FontWeight.w500)),
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.blue, width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6)),
),
),
),
),
const SizedBox(height: 15),
TextField(
onChanged: (value) => setState(() {
_searchQuery = value;
_currentPage = 0;
}),
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari nama balita...",
prefixIcon: const Icon(Icons.search, size: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(vertical: 0),
),
),
const SizedBox(height: 20),
Row(
children: [
const Icon(Icons.swipe_left_alt,
size: 14, color: Colors.grey),
const SizedBox(width: 5),
Text("Geser ke kanan untuk melihat lebih lanjut",
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic)),
],
),
const SizedBox(height: 5),
Expanded(
child: FutureBuilder<Map<String, dynamic>>(
future: fetchPemeriksaanBalita(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return const Center(child: CircularProgressIndicator());
if (snapshot.hasError)
return Center(child: Text("Error: ${snapshot.error}"));
final List<dynamic> rawData = snapshot.data?['data'] ?? [];
final filteredData = rawData
.where((item) => item["nama"]
.toString()
.toLowerCase()
.contains(_searchQuery.toLowerCase()))
.toList();
if (filteredData.isEmpty)
return const Center(child: Text("Data tidak ditemukan"));
final totalPages =
(filteredData.length / _rowsPerPage).ceil();
final start = _currentPage * _rowsPerPage;
final end = (start + _rowsPerPage > filteredData.length)
? filteredData.length
: start + _rowsPerPage;
final paginatedData = filteredData.sublist(start, end);
return Column(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _buildTable(paginatedData, start),
),
),
),
_buildPaginationControls(totalPages),
],
);
},
),
),
],
),
),
),
);
}
Widget _buildTable(List<dynamic> dataList, int startIndex) {
return DataTable(
headingRowColor: WidgetStateProperty.all(Colors.blue.shade600),
columnSpacing: 18,
horizontalMargin: 10,
columns: [
_headerCell("No"),
_headerCell("NIK"),
_headerCell("Nama Balita"),
_headerCell("Nama Orang Tua"),
_headerCell("TTL"),
_headerCell("Umur"),
_headerCell("JK"),
_headerCell("Anak Ke"),
_headerCell("Alamat"),
_headerCell("Tgl Periksa"),
_headerCell("BB (kg)"),
_headerCell("TB (cm)"),
_headerCell("LK (cm)"),
_headerCell("Imunisasi"),
_headerCell("BB/U"),
_headerCell("TB/U"),
_headerCell("BB/TB"),
_headerCell("Kehadiran"),
],
rows: List.generate(dataList.length, (index) {
final data = dataList[index];
return DataRow(cells: [
DataCell(_textCell((startIndex + index + 1).toString())),
DataCell(_textCell(data['nik_balita'] ?? "-")),
DataCell(_textCell(data['nama'] ?? "-")),
DataCell(_textCell(data['nama_orang_tua'] ?? "-")),
DataCell(_textCell(
"${data['tempat_lahir'] ?? ''}, ${data['tgl_lahir'] ?? ''}")),
DataCell(_textCell("${data['umur'] ?? '-'} Bln")),
DataCell(_textCell(data['jenis_kelamin'] ?? "-")),
DataCell(_textCell(data['anak_ke']?.toString() ?? "-")),
DataCell(_textCell(data['alamat_lengkap'] ?? "-")),
DataCell(_textCell(data['tgl_periksa'] ?? "-")),
DataCell(_textCell(data['berat_badan']?.toString() ?? "-")),
DataCell(_textCell(data['tinggi_badan']?.toString() ?? "-")),
DataCell(_textCell(data['lingkar_kepala']?.toString() ?? "-")),
DataCell(_textCell(data['pemberian_imunisasi'] ?? "-")),
DataCell(_statusGiziBadge(data['status_bbu'])),
DataCell(_statusGiziBadge(data['status_tbu'])),
DataCell(_statusGiziBadge(data['status_bbtb'])),
DataCell(_textCell(data['kehadiran_posyandu'] ?? "-")),
]);
}),
);
}
DataColumn _headerCell(String label) => DataColumn(
label: Text(label,
style: GoogleFonts.poppins(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11)));
Widget _textCell(String text) =>
Text(text, style: GoogleFonts.poppins(fontSize: 11));
Widget _statusGiziBadge(String? status) {
if (status == null || status == "-") return _textCell("-");
String normalValue = status.toLowerCase();
Color color = Colors.grey;
if (normalValue.contains('baik') || normalValue.contains('normal')) {
color = Colors.green;
} else if (normalValue.contains('kurang') ||
normalValue.contains('pendek')) {
color = Colors.orange;
} else if (normalValue.contains('buruk') ||
normalValue.contains('sangat pendek') ||
normalValue.contains('wasting')) {
color = Colors.red;
} else if (normalValue.contains('lebih') ||
normalValue.contains('obesitas') ||
normalValue.contains('overweight')) {
color = Colors.purple;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
child: Text(status,
style: GoogleFonts.poppins(
color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold)),
);
}
Widget _buildPaginationControls(int totalPages) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(fontSize: 11)),
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 16),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--)),
IconButton(
icon: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++)),
],
),
],
),
);
}
}