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 createState() => _DataLaporanPageStatus(); } class _DataLaporanPageStatus extends State { int _currentPage = 0; final int _rowsPerPage = 10; String _searchQuery = ""; List _allData = []; final List _namaBulan = [ "Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember" ]; Future> 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 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 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 _generateFilteredPdf(List selectedMonths) async { List 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 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>.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>( 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 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 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++)), ], ), ], ), ); } }