Klimatologiot/lib/screens/monitoring/shared/utils/pdf/pdf_components.dart

285 lines
9.1 KiB
Dart

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:intl/intl.dart';
/// ============================================================
/// PDF Components — Reusable builders
/// Letakkan di: lib/screens/monitoring/shared/utils/pdf/pdf_components.dart
/// ============================================================
// ─── Warna tema ───────────────────────────────────────────────
const kPdfPrimary = PdfColor.fromInt(0xFF1565C0);
const kPdfAccent = PdfColor.fromInt(0xFF42A5F5);
const kPdfRowEven = PdfColor.fromInt(0xFFF5F9FF);
const kPdfBorder = PdfColor.fromInt(0xFFBBDEFB);
// ─── Format tanggal ───────────────────────────────────────────
final pdfDateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
final pdfTimeFormat = DateFormat('HH:mm:ss');
final pdfFullFormat = DateFormat('dd MMM yyyy, HH:mm', 'id_ID');
// ================================================================
// HEADER — muncul di setiap halaman
// ================================================================
pw.Widget buildPdfHeader(String title, DateTime date) {
return pw.Container(
margin: const pw.EdgeInsets.only(bottom: 16),
decoration: const pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(color: kPdfBorder, width: 1.5),
),
),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
title,
style: pw.TextStyle(
fontSize: 18,
fontWeight: pw.FontWeight.bold,
color: kPdfPrimary,
),
),
pw.SizedBox(height: 2),
pw.Text(
'Sistem Monitoring Klimatologi',
style: pw.TextStyle(fontSize: 10, color: PdfColors.grey600),
),
],
),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
pdfDateFormat.format(date),
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: kPdfPrimary,
),
),
pw.Text(
pdfTimeFormat.format(date),
style: pw.TextStyle(fontSize: 9, color: PdfColors.grey600),
),
],
),
],
),
);
}
// ================================================================
// FOOTER — muncul di setiap halaman
// ================================================================
pw.Widget buildPdfFooter(pw.Context context) {
return pw.Container(
margin: const pw.EdgeInsets.only(top: 12),
decoration: const pw.BoxDecoration(
border: pw.Border(top: pw.BorderSide(color: kPdfBorder, width: 1)),
),
child: pw.Padding(
padding: const pw.EdgeInsets.only(top: 6),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Digenerate otomatis oleh Aplikasi Klimatologi',
style: pw.TextStyle(fontSize: 8, color: PdfColors.grey500),
),
pw.Text(
'Halaman ${context.pageNumber} / ${context.pagesCount}',
style: pw.TextStyle(fontSize: 8, color: PdfColors.grey500),
),
],
),
),
);
}
// ================================================================
// SECTION TITLE — judul bagian dengan background biru
// ================================================================
pw.Widget buildSectionTitle(String title) {
return pw.Container(
padding: const pw.EdgeInsets.symmetric(vertical: 6, horizontal: 10),
decoration: const pw.BoxDecoration(
color: kPdfPrimary,
borderRadius: pw.BorderRadius.all(pw.Radius.circular(4)),
),
child: pw.Text(
title,
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white,
),
),
);
}
// ================================================================
// SUMMARY CARD — grid kartu nilai terkini
// ================================================================
pw.Widget buildSummaryCard({
required String title,
required String subtitle,
required List<SummaryItem> items,
}) {
return pw.Container(
padding: const pw.EdgeInsets.all(16),
decoration: pw.BoxDecoration(
color: const PdfColor.fromInt(0xFFE3F2FD),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(8)),
border: pw.Border.all(color: kPdfBorder),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
title,
style: pw.TextStyle(
fontSize: 13,
fontWeight: pw.FontWeight.bold,
color: kPdfPrimary,
),
),
pw.Text(
subtitle,
style: pw.TextStyle(fontSize: 9, color: PdfColors.grey600),
),
pw.SizedBox(height: 12),
pw.Wrap(
spacing: 12,
runSpacing: 8,
children: items.map((item) => _summaryItemCard(item)).toList(),
),
],
),
);
}
pw.Widget _summaryItemCard(SummaryItem item) {
return pw.Container(
width: 200,
padding: const pw.EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: pw.BoxDecoration(
color: PdfColors.white,
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
border: pw.Border.all(color: kPdfBorder),
),
child: pw.Row(
children: [
pw.Text(item.emoji, style: const pw.TextStyle(fontSize: 18)),
pw.SizedBox(width: 8),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
item.label,
style: pw.TextStyle(fontSize: 9, color: PdfColors.grey600),
),
pw.Text(
item.value,
style: pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
color: kPdfPrimary,
),
),
],
),
],
),
);
}
// ================================================================
// BASE TABLE — semua tabel pakai ini
// ================================================================
pw.Widget buildDataTable({
required List<String> headers,
required List<List<String>> rows,
Map<int, pw.TableColumnWidth>? colWidths,
}) {
return pw.Table(
border: pw.TableBorder.all(color: kPdfBorder, width: 0.5),
columnWidths: colWidths,
children: [
// Header
pw.TableRow(
decoration: const pw.BoxDecoration(color: kPdfPrimary),
children: headers
.map((h) => pw.Padding(
padding: const pw.EdgeInsets.symmetric(
vertical: 7, horizontal: 8),
child: pw.Text(
h,
style: pw.TextStyle(
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white,
),
),
))
.toList(),
),
// Data rows
...rows.asMap().entries.map((entry) {
final isEven = entry.key % 2 == 0;
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? kPdfRowEven : PdfColors.white,
),
children: entry.value
.map((cell) => pw.Padding(
padding: const pw.EdgeInsets.symmetric(
vertical: 5, horizontal: 8),
child: pw.Text(cell,
style: const pw.TextStyle(fontSize: 9)),
))
.toList(),
);
}),
],
);
}
// ================================================================
// KEY-VALUE TABLE — tabel 3 kolom: Parameter | Nilai | Satuan
// ================================================================
pw.Widget buildKeyValueTable(List<List<String>> rows) {
return buildDataTable(
headers: rows.first,
rows: rows.skip(1).toList(),
colWidths: {
0: const pw.FlexColumnWidth(2),
1: const pw.FlexColumnWidth(1.5),
2: const pw.FlexColumnWidth(1),
},
);
}
// ================================================================
// HELPER CLASS
// ================================================================
class SummaryItem {
final String label;
final String value;
final String emoji;
const SummaryItem(this.label, this.value, this.emoji);
}
// ================================================================
// STATS HELPER
// ================================================================
Map<String, double> calcStats(List<double> data) {
if (data.isEmpty) return {'avg': 0, 'max': 0, 'min': 0};
final sorted = List<double>.from(data)..sort();
final avg = data.reduce((a, b) => a + b) / data.length;
return {'avg': avg, 'max': sorted.last, 'min': sorted.first};
}