joss banget anjing

This commit is contained in:
kleponijo 2026-04-25 07:28:05 +07:00
parent 4b01a7e41b
commit a08bb1e710
18 changed files with 951 additions and 4 deletions

View File

@ -6,12 +6,14 @@ import 'package:klimatologiot/app.dart';
import 'package:user_repository/user_repository.dart'; import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart'; import 'package:monitoring_repository/monitoring_repository.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
await initializeDateFormatting('id_ID', null);
Bloc.observer = SimpleBlocObserver(); Bloc.observer = SimpleBlocObserver();
runApp(MyApp( runApp(MyApp(
FirebaseUserRepo(), FirebaseUserRepo(),

View File

@ -0,0 +1,90 @@
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import 'pdf_components.dart';
/// ============================================================
/// Atmospheric PDF Builder
/// Letakkan di: lib/screens/monitoring/shared/utils/pdf/atmospheric_pdf_builder.dart
/// ============================================================
Future<void> exportAtmosphericPdf({
required double temperature,
required double humidity,
required double pressure,
required double altitude,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) async {
final pdf = pw.Document();
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
header: (ctx) => buildPdfHeader('Laporan Kondisi Atmosfer', timestamp),
footer: (ctx) => buildPdfFooter(ctx),
build: (ctx) => [
// Summary card
buildSummaryCard(
title: 'Data Terkini',
subtitle: pdfFullFormat.format(timestamp),
items: [
SummaryItem('Suhu', '${temperature.toStringAsFixed(1)} °C', '🌡️'),
SummaryItem('Kelembapan', '${humidity.toStringAsFixed(1)} %', '💧'),
SummaryItem('Tekanan', '${pressure.toStringAsFixed(1)} hPa', '🔵'),
SummaryItem('Ketinggian', '${altitude.toStringAsFixed(1)} m', '⛰️'),
],
),
pw.SizedBox(height: 24),
// Tabel key-value ringkasan
buildSectionTitle('Detail Parameter'),
pw.SizedBox(height: 8),
buildKeyValueTable([
['Parameter', 'Nilai', 'Satuan'],
['Suhu', temperature.toStringAsFixed(1), '°C'],
['Kelembapan', humidity.toStringAsFixed(1), '%'],
['Tekanan Udara', pressure.toStringAsFixed(1), 'hPa'],
['Ketinggian', altitude.toStringAsFixed(1), 'm'],
]),
// Tabel history jika ada
if (historyData != null && historyData.isNotEmpty) ...[
pw.SizedBox(height: 24),
buildSectionTitle('Riwayat Data'),
pw.SizedBox(height: 8),
_buildHistoryTable(historyData),
],
],
),
);
await Printing.layoutPdf(onLayout: (_) => pdf.save());
}
pw.Widget _buildHistoryTable(List<Map<String, dynamic>> data) {
return buildDataTable(
headers: ['No', 'Waktu', 'Suhu (°C)', 'Kelembapan (%)', 'Tekanan (hPa)', 'Ketinggian (m)'],
rows: data.asMap().entries.map((e) {
final d = e.value;
final ts = d['timestamp'] as DateTime?;
return [
'${e.key + 1}',
ts != null ? pdfFullFormat.format(ts) : '-',
(d['temperature'] as double? ?? 0).toStringAsFixed(1),
(d['humidity'] as double? ?? 0).toStringAsFixed(1),
(d['pressure'] as double? ?? 0).toStringAsFixed(1),
(d['altitude'] as double? ?? 0).toStringAsFixed(1),
];
}).toList(),
colWidths: {
0: const pw.FixedColumnWidth(25),
1: const pw.FlexColumnWidth(2),
2: const pw.FlexColumnWidth(1),
3: const pw.FlexColumnWidth(1.2),
4: const pw.FlexColumnWidth(1.2),
5: const pw.FlexColumnWidth(1.2),
},
);
}

View File

@ -0,0 +1,85 @@
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import 'pdf_components.dart';
/// ============================================================
/// Evaporasi PDF Builder
/// Letakkan di: lib/screens/monitoring/shared/utils/pdf/evaporasi_pdf_builder.dart
/// ============================================================
Future<void> exportEvaporasiPdf({
required double evaporasi,
required double suhu,
required double tinggiAir,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) async {
final pdf = pw.Document();
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
header: (ctx) => buildPdfHeader('Laporan Evaporasi', timestamp),
footer: (ctx) => buildPdfFooter(ctx),
build: (ctx) => [
// Summary card
buildSummaryCard(
title: 'Data Terkini',
subtitle: pdfFullFormat.format(timestamp),
items: [
SummaryItem('Evaporasi', '${evaporasi.toStringAsFixed(2)} mm', '💧'),
SummaryItem('Suhu', '${suhu.toStringAsFixed(1)} °C', '🌡️'),
SummaryItem('Tinggi Air', '${tinggiAir.toStringAsFixed(1)} cm', '📏'),
],
),
pw.SizedBox(height: 24),
// Tabel key-value ringkasan
buildSectionTitle('Detail Parameter'),
pw.SizedBox(height: 8),
buildKeyValueTable([
['Parameter', 'Nilai', 'Satuan'],
['Evaporasi', evaporasi.toStringAsFixed(2), 'mm'],
['Suhu', suhu.toStringAsFixed(1), '°C'],
['Tinggi Air', tinggiAir.toStringAsFixed(1), 'cm'],
]),
// Tabel history jika ada
if (historyData != null && historyData.isNotEmpty) ...[
pw.SizedBox(height: 24),
buildSectionTitle('Riwayat Data'),
pw.SizedBox(height: 8),
_buildHistoryTable(historyData),
],
],
),
);
await Printing.layoutPdf(onLayout: (_) => pdf.save());
}
pw.Widget _buildHistoryTable(List<Map<String, dynamic>> data) {
return buildDataTable(
headers: ['No', 'Waktu', 'Evaporasi (mm)', 'Suhu (°C)', 'Tinggi Air (cm)'],
rows: data.asMap().entries.map((e) {
final d = e.value;
final ts = d['timestamp'] as DateTime?;
return [
'${e.key + 1}',
ts != null ? pdfFullFormat.format(ts) : '-',
(d['evaporasi'] as double? ?? 0).toStringAsFixed(2),
(d['suhu'] as double? ?? 0).toStringAsFixed(1),
(d['tinggiAir'] as double? ?? 0).toStringAsFixed(1),
];
}).toList(),
colWidths: {
0: const pw.FixedColumnWidth(25),
1: const pw.FlexColumnWidth(2),
2: const pw.FlexColumnWidth(1.3),
3: const pw.FlexColumnWidth(1),
4: const pw.FlexColumnWidth(1.3),
},
);
}

View File

@ -0,0 +1,284 @@
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};
}

View File

@ -0,0 +1,62 @@
import 'atmospheric_pdf_builder.dart';
import 'evaporasi_pdf_builder.dart';
import 'wind_speed_pdf_builder.dart';
/// ============================================================
/// PDF Export Service Entry Point
/// Letakkan di: lib/screens/monitoring/shared/utils/pdf/pdf_export_service.dart
///
/// Ini cuma facade semua logic ada di builder masing-masing.
/// Import file ini saja di screen kamu.
/// ============================================================
class PdfExportService {
PdfExportService._(); // tidak bisa di-instantiate
static Future<void> atmospheric({
required double temperature,
required double humidity,
required double pressure,
required double altitude,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) =>
exportAtmosphericPdf(
temperature: temperature,
humidity: humidity,
pressure: pressure,
altitude: altitude,
timestamp: timestamp,
historyData: historyData,
);
static Future<void> evaporasi({
required double evaporasi,
required double suhu,
required double tinggiAir,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) =>
exportEvaporasiPdf(
evaporasi: evaporasi,
suhu: suhu,
tinggiAir: tinggiAir,
timestamp: timestamp,
historyData: historyData,
);
static Future<void> windSpeed({
required double currentSpeed,
required String period,
required List<double> speeds,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) =>
exportWindSpeedPdf(
currentSpeed: currentSpeed,
period: period,
speeds: speeds,
timestamp: timestamp,
historyData: historyData,
);
}

View File

@ -0,0 +1,110 @@
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import 'pdf_components.dart';
/// ============================================================
/// Wind Speed PDF Builder
/// Letakkan di: lib/screens/monitoring/shared/utils/pdf/wind_speed_pdf_builder.dart
/// ============================================================
Future<void> exportWindSpeedPdf({
required double currentSpeed,
required String period,
required List<double> speeds,
required DateTime timestamp,
List<Map<String, dynamic>>? historyData,
}) async {
final pdf = pw.Document();
final stats = calcStats(speeds);
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
header: (ctx) => buildPdfHeader('Laporan Kecepatan Angin', timestamp),
footer: (ctx) => buildPdfFooter(ctx),
build: (ctx) => [
// Summary card
buildSummaryCard(
title: 'Data Terkini',
subtitle: 'Periode: $period',
items: [
SummaryItem('Kecepatan', '${currentSpeed.toStringAsFixed(1)} m/s', '💨'),
SummaryItem('Rata-rata', '${stats['avg']!.toStringAsFixed(1)} m/s', '📊'),
SummaryItem('Maksimum', '${stats['max']!.toStringAsFixed(1)} m/s', '⬆️'),
SummaryItem('Minimum', '${stats['min']!.toStringAsFixed(1)} m/s', '⬇️'),
],
),
pw.SizedBox(height: 24),
// Tabel data per periode
if (speeds.isNotEmpty) ...[
buildSectionTitle('Data Periode ($period)'),
pw.SizedBox(height: 8),
_buildPeriodTable(speeds, period),
],
// Tabel history jika ada
if (historyData != null && historyData.isNotEmpty) ...[
pw.SizedBox(height: 24),
buildSectionTitle('Riwayat Detail'),
pw.SizedBox(height: 8),
_buildHistoryTable(historyData),
],
],
),
);
await Printing.layoutPdf(onLayout: (_) => pdf.save());
}
pw.Widget _buildPeriodTable(List<double> speeds, String period) {
final labels = _periodLabels(speeds.length, period);
return buildDataTable(
headers: ['No', 'Label', 'Kecepatan (m/s)'],
rows: speeds.asMap().entries.map((e) => [
'${e.key + 1}',
labels[e.key],
e.value.toStringAsFixed(2),
]).toList(),
colWidths: {
0: const pw.FixedColumnWidth(30),
1: const pw.FlexColumnWidth(2),
2: const pw.FlexColumnWidth(1.5),
},
);
}
pw.Widget _buildHistoryTable(List<Map<String, dynamic>> data) {
return buildDataTable(
headers: ['No', 'Waktu', 'Kecepatan (m/s)', 'Pulse'],
rows: data.asMap().entries.map((e) {
final d = e.value;
final ts = d['timestamp'] as DateTime?;
return [
'${e.key + 1}',
ts != null ? pdfFullFormat.format(ts) : '-',
(d['speed'] as double? ?? 0).toStringAsFixed(2),
'${d['pulse'] ?? 0}',
];
}).toList(),
colWidths: {
0: const pw.FixedColumnWidth(25),
1: const pw.FlexColumnWidth(2.5),
2: const pw.FlexColumnWidth(1.5),
3: const pw.FlexColumnWidth(1),
},
);
}
List<String> _periodLabels(int count, String period) {
if (period == 'Hari Ini') {
return List.generate(count, (i) => 'Jam ${i.toString().padLeft(2, '0')}:00');
} else if (period == 'Minggu Ini') {
const days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
return List.generate(count, (i) => days[i % 7]);
} else {
return List.generate(count, (i) => 'Minggu ${i + 1}');
}
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
/// ============================================================
/// Export PDF Button Reusable Widget
/// Letakkan di: lib/screens/monitoring/shared/widgets/export_pdf_button.dart
/// ============================================================
///
/// Cara pakai (contoh di WindSpeedScreen):
///
/// ExportPdfButton(
/// onExport: () async {
/// await PdfExportService.exportWindSpeedPdf(
/// currentSpeed: state.currentSpeed,
/// period: state.selectedPeriod,
/// speeds: data,
/// timestamp: DateTime.now(),
/// );
/// },
/// )
class ExportPdfButton extends StatefulWidget {
/// Callback async yang memanggil PdfExportService
final Future<void> Function() onExport;
/// Label tombol (default: "Export PDF")
final String label;
/// Style: FAB (floating) atau ElevatedButton (inline)
final ExportButtonStyle style;
const ExportPdfButton({
super.key,
required this.onExport,
this.label = 'Export PDF',
this.style = ExportButtonStyle.elevated,
});
@override
State<ExportPdfButton> createState() => _ExportPdfButtonState();
}
class _ExportPdfButtonState extends State<ExportPdfButton> {
bool _isLoading = false;
Future<void> _handleExport() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
await widget.onExport();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal export PDF: $e'),
backgroundColor: Colors.red.shade600,
),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return widget.style == ExportButtonStyle.fab
? _buildFab()
: _buildElevated();
}
// FAB style (floating action button)
Widget _buildFab() {
return FloatingActionButton.extended(
onPressed: _isLoading ? null : _handleExport,
backgroundColor: Colors.blue.shade700,
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.picture_as_pdf, color: Colors.white),
label: Text(
_isLoading ? 'Menyiapkan...' : widget.label,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
);
}
// Elevated button style (inline di dalam konten)
Widget _buildElevated() {
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _handleExport,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
icon: _isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.picture_as_pdf, color: Colors.white),
label: Text(
_isLoading ? 'Menyiapkan PDF...' : widget.label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
);
}
}
enum ExportButtonStyle { fab, elevated }

View File

@ -48,9 +48,35 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
); );
final dailyGraph = TimeSeriesMapper.smooth(raw); final dailyGraph = TimeSeriesMapper.smooth(raw);
final daily = TimeSeriesMapper.smooth(
TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
final weekly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
final monthly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
emit(state.copyWith( emit(state.copyWith(
history: history, history: history,
dailySpeeds: dailyGraph, dailySpeeds: dailyGraph,
weeklySpeeds: weekly,
monthlySpeeds: monthly,
isLoading: false, isLoading: false,
)); ));
@ -91,7 +117,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
} }
} }
updated[index] = newValue; updated[index] = newValue.clamp(0, 100);
} }
emit(state.copyWith( emit(state.copyWith(
@ -107,7 +133,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
WindSpeedPeriodChanged event, WindSpeedPeriodChanged event,
Emitter<WindSpeedState> emit, Emitter<WindSpeedState> emit,
) async { ) async {
emit(state.copyWith(isLoading: true, selectedPeriod: event.period)); emit(state.copyWith(selectedPeriod: event.period));
final history = state.history; final history = state.history;

View File

@ -3,7 +3,9 @@ part of 'wind_speed_bloc.dart';
class WindSpeedState extends Equatable { class WindSpeedState extends Equatable {
final double currentSpeed; final double currentSpeed;
final String selectedPeriod; final String selectedPeriod;
final List<double> dailySpeeds; // Untuk grafik final List<double> dailySpeeds;
final List<double> weeklySpeeds;
final List<double> monthlySpeeds;
final bool isLoading; final bool isLoading;
final List<MyWindSpeed> history; final List<MyWindSpeed> history;
@ -13,12 +15,16 @@ class WindSpeedState extends Equatable {
this.dailySpeeds = const [], this.dailySpeeds = const [],
this.isLoading = true, this.isLoading = true,
this.history = const [], this.history = const [],
this.monthlySpeeds = const [],
this.weeklySpeeds = const [],
}); });
WindSpeedState copyWith({ WindSpeedState copyWith({
double? currentSpeed, double? currentSpeed,
String? selectedPeriod, String? selectedPeriod,
List<double>? dailySpeeds, List<double>? dailySpeeds,
List<double>? weeklySpeeds,
List<double>? monthlySpeeds,
bool? isLoading, bool? isLoading,
List<MyWindSpeed>? history, List<MyWindSpeed>? history,
}) { }) {
@ -26,6 +32,8 @@ class WindSpeedState extends Equatable {
currentSpeed: currentSpeed ?? this.currentSpeed, currentSpeed: currentSpeed ?? this.currentSpeed,
selectedPeriod: selectedPeriod ?? this.selectedPeriod, selectedPeriod: selectedPeriod ?? this.selectedPeriod,
dailySpeeds: dailySpeeds ?? this.dailySpeeds, dailySpeeds: dailySpeeds ?? this.dailySpeeds,
weeklySpeeds: weeklySpeeds ?? this.weeklySpeeds,
monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
history: history ?? this.history, history: history ?? this.history,
); );

View File

@ -5,6 +5,9 @@ import '../blocs/wind_speed_bloc.dart';
import './widgets/wind_speed_chart_widget.dart'; import './widgets/wind_speed_chart_widget.dart';
import 'widgets/period_selector.dart'; import 'widgets/period_selector.dart';
import '../../shared/utils/pdf/pdf_export_service.dart';
import '../../shared/widgets/export_pdf_button.dart';
class WindSpeedScreen extends StatelessWidget { class WindSpeedScreen extends StatelessWidget {
const WindSpeedScreen({super.key}); const WindSpeedScreen({super.key});
@ -30,6 +33,32 @@ class WindSpeedScreen extends StatelessWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// Pilih data sesuai periode
final List<double> data = switch (state.selectedPeriod) {
"Minggu Ini" => state.weeklySpeeds,
"Bulan Ini" => state.monthlySpeeds,
_ => state.dailySpeeds,
};
// List<double> data;
// if (state.selectedPeriod == "Minggu Ini") {
// data = state.weeklySpeeds;
// } else if (state.selectedPeriod == "Bulan Ini") {
// data = state.monthlySpeeds;
// } else {
// data = state.dailySpeeds;
// }
// Konversi history MyWindSpeed Map (untuk tabel PDF)
final historyMaps = state.history
.map((e) => {
'timestamp': e.timestamp,
'speed': e.speed,
'pulse': e.pulse,
})
.toList();
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
@ -46,7 +75,7 @@ class WindSpeedScreen extends StatelessWidget {
const SizedBox(height: 15), const SizedBox(height: 15),
WindSpeedChartWidget( WindSpeedChartWidget(
dailySpeeds: state.dailySpeeds, dailySpeeds: data,
period: state.selectedPeriod, period: state.selectedPeriod,
), ),
@ -54,6 +83,18 @@ class WindSpeedScreen extends StatelessWidget {
// 3. Info Tambahan (Status/Periode) // 3. Info Tambahan (Status/Periode)
_buildDetailRow(state.selectedPeriod), _buildDetailRow(state.selectedPeriod),
const SizedBox(height: 24),
// Tombol Export PDF
ExportPdfButton(
onExport: () => PdfExportService.windSpeed(
currentSpeed: state.currentSpeed,
period: state.selectedPeriod,
speeds: data,
timestamp: DateTime.now(),
historyData: historyMaps.isNotEmpty ? historyMaps : null,
),
),
], ],
), ),
); );

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <printing/printing_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
printing
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -10,6 +10,7 @@ import firebase_auth
import firebase_core import firebase_core
import firebase_database import firebase_database
import google_sign_in_ios import google_sign_in_ios
import printing
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
} }

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.66" version: "1.3.66"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -25,6 +33,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -352,6 +376,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -487,6 +519,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -535,6 +575,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
url: "https://pub.dev"
source: hosted
version: "3.12.0"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -551,6 +615,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
printing:
dependency: "direct main"
description:
name: printing
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
url: "https://pub.dev"
source: hosted
version: "5.14.3"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -567,6 +647,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
rxdart: rxdart:
dependency: "direct main" dependency: "direct main"
description: description:
@ -683,6 +771,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@ -25,6 +25,8 @@ dependencies:
rxdart: ^0.27.7 rxdart: ^0.27.7
user_repository: user_repository:
path: packages/user_repository path: packages/user_repository
pdf: ^3.11.1
printing: ^5.13.1
dev_dependencies: dev_dependencies:
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0

View File

@ -9,6 +9,7 @@
#include <cloud_firestore/cloud_firestore_plugin_c_api.h> #include <cloud_firestore/cloud_firestore_plugin_c_api.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h> #include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <printing/printing_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
CloudFirestorePluginCApiRegisterWithRegistrar( CloudFirestorePluginCApiRegisterWithRegistrar(
@ -17,4 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar( FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
} }

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
cloud_firestore cloud_firestore
firebase_auth firebase_auth
firebase_core firebase_core
printing
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST