Merge branch 'branch_percobaan'
This commit is contained in:
commit
0cce4da44b
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "klimatologi",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "klimatologi (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "klimatologi (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "monitoring_repository",
|
||||||
|
"cwd": "packages\\monitoring_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "monitoring_repository (profile mode)",
|
||||||
|
"cwd": "packages\\monitoring_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "monitoring_repository (release mode)",
|
||||||
|
"cwd": "packages\\monitoring_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_repository",
|
||||||
|
"cwd": "packages\\user_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_repository (profile mode)",
|
||||||
|
"cwd": "packages\\user_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_repository (release mode)",
|
||||||
|
"cwd": "packages\\user_repository",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "219079385989-qtvssir2q3c4lo6ijn3p07kl77ukdme2.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.example.klimatologiot",
|
||||||
|
"certificate_hash": "f5be897daa6ad87f2bd9c04560f67ef379eac6fe"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "219079385989-8hkp1kg2pprs5k269s4c08r6rpdj1gjd.apps.googleusercontent.com",
|
"client_id": "219079385989-8hkp1kg2pprs5k269s4c08r6rpdj1gjd.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -0,0 +1,117 @@
|
||||||
|
class TimeSeriesMapper {
|
||||||
|
/// =========================
|
||||||
|
/// 📅 DAILY (24 JAM)
|
||||||
|
/// =========================
|
||||||
|
static List<double> toDaily<T>({
|
||||||
|
required List<T> data,
|
||||||
|
required DateTime Function(T) getTime,
|
||||||
|
required double Function(T) getValue,
|
||||||
|
}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
final sums = List<double>.filled(24, 0.0);
|
||||||
|
final counts = List<int>.filled(24, 0);
|
||||||
|
|
||||||
|
for (final item in data) {
|
||||||
|
final time = getTime(item);
|
||||||
|
|
||||||
|
if (_isSameDay(time, now)) {
|
||||||
|
final hour = time.hour;
|
||||||
|
sums[hour] += getValue(item);
|
||||||
|
counts[hour]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.generate(24, (i) {
|
||||||
|
if (counts[i] == 0) return 0;
|
||||||
|
return sums[i] / counts[i]; // 🔥 average, bukan overwrite
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📅 WEEKLY (7 HARI)
|
||||||
|
/// =========================
|
||||||
|
static List<double> toWeekly<T>({
|
||||||
|
required List<T> data,
|
||||||
|
required DateTime Function(T) getTime,
|
||||||
|
required double Function(T) getValue,
|
||||||
|
}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
final sums = List<double>.filled(7, 0.0);
|
||||||
|
final counts = List<int>.filled(7, 0);
|
||||||
|
|
||||||
|
DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
|
||||||
|
startOfWeek =
|
||||||
|
DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day);
|
||||||
|
|
||||||
|
for (final item in data) {
|
||||||
|
final time = getTime(item);
|
||||||
|
|
||||||
|
if (time.isAfter(startOfWeek)) {
|
||||||
|
final index = time.weekday - 1;
|
||||||
|
sums[index] += getValue(item);
|
||||||
|
counts[index]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.generate(7, (i) {
|
||||||
|
if (counts[i] == 0) return 0;
|
||||||
|
return sums[i] / counts[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📅 MONTHLY
|
||||||
|
/// =========================
|
||||||
|
static List<double> toMonthly<T>({
|
||||||
|
required List<T> data,
|
||||||
|
required DateTime Function(T) getTime,
|
||||||
|
required double Function(T) getValue,
|
||||||
|
}) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
|
||||||
|
|
||||||
|
final sums = List<double>.filled(daysInMonth, 0.0);
|
||||||
|
final counts = List<int>.filled(daysInMonth, 0);
|
||||||
|
|
||||||
|
for (final item in data) {
|
||||||
|
final time = getTime(item);
|
||||||
|
|
||||||
|
if (time.month == now.month && time.year == now.year) {
|
||||||
|
final index = time.day - 1;
|
||||||
|
sums[index] += getValue(item);
|
||||||
|
counts[index]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.generate(daysInMonth, (i) {
|
||||||
|
if (counts[i] == 0) return 0;
|
||||||
|
return sums[i] / counts[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🧠 HELPER
|
||||||
|
/// =========================
|
||||||
|
static bool _isSameDay(DateTime a, DateTime b) {
|
||||||
|
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<double> smooth(List<double> data) {
|
||||||
|
if (data.length < 3) return data;
|
||||||
|
|
||||||
|
List<double> result = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
if (i == 0 || i == data.length - 1) {
|
||||||
|
result.add(data[i]);
|
||||||
|
} else {
|
||||||
|
final avg = (data[i - 1] + data[i] + data[i + 1]) / 3;
|
||||||
|
result.add(avg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
||||||
emit(SignInProcess());
|
emit(SignInProcess());
|
||||||
try {
|
try {
|
||||||
await _userRepository.signInWithGoogle();
|
await _userRepository.signInWithGoogle();
|
||||||
|
emit(SignInSuccess());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(SignInFailure());
|
emit(SignInFailure());
|
||||||
emit(SignInInitial());
|
emit(SignInInitial());
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,12 @@ class HomeScreen extends StatelessWidget {
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
// Ganti dengan Image.asset jika sudah ada logo
|
// Ganti dengan Image.asset jika sudah ada logo
|
||||||
const Icon(Icons.cloud_queue, color: Colors.blue, size: 28),
|
Image.asset(
|
||||||
const SizedBox(width: 10),
|
'images/logo_klimatologi.png',
|
||||||
Text(
|
height: 90,
|
||||||
"Klimatologi",
|
fit: BoxFit.contain,
|
||||||
style: GoogleFonts.rubik(
|
|
||||||
color: Colors.black,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:klimatologiot/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||||
import '../../monitoring/wind_speed/views/wind_speed_screen.dart';
|
import '../../monitoring/wind_speed/views/wind_speed_screen.dart';
|
||||||
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
|
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
|
||||||
|
import '../../monitoring/evaporasi/views/evaporasi_screen.dart';
|
||||||
|
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
|
||||||
|
import '../../monitoring/atmospheric_conditions/views/atmospheric_screen.dart';
|
||||||
|
|
||||||
class MainDrawer extends StatelessWidget {
|
class MainDrawer extends StatelessWidget {
|
||||||
const MainDrawer({super.key});
|
const MainDrawer({super.key});
|
||||||
|
|
@ -68,7 +72,20 @@ class MainDrawer extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.water_drop),
|
leading: const Icon(Icons.water_drop),
|
||||||
title: const Text("Evaporasi"),
|
title: const Text("Evaporasi"),
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider<EvaporasiBloc>(
|
||||||
|
create: (context) => EvaporasiBloc(
|
||||||
|
repository: context.read<MonitoringRepository>(),
|
||||||
|
)..add(WatchEvaporasiStarted()),
|
||||||
|
child: const EvaporasiScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Image.asset(
|
leading: Image.asset(
|
||||||
|
|
@ -76,8 +93,21 @@ class MainDrawer extends StatelessWidget {
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
),
|
),
|
||||||
title: const Text("Tekanan Udara"),
|
title: const Text("Kondisi Atmosfer"),
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider<AtmosphericConditionsBloc>(
|
||||||
|
create: (context) => AtmosphericConditionsBloc(
|
||||||
|
repository: context.read<MonitoringRepository>(),
|
||||||
|
)..add(WatchAtmosphericConditionsStarted()),
|
||||||
|
child: AtmosphericScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Spacer(), // Dorong menu logout ke paling bawah
|
const Spacer(), // Dorong menu logout ke paling bawah
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
|
||||||
|
|
@ -1,784 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
import 'shared/widgets/monitoring_shared.dart';
|
|
||||||
|
|
||||||
/// AIR QUALITY MONITORING SCREEN
|
|
||||||
class AirQualityMonitoringScreen extends StatefulWidget {
|
|
||||||
const AirQualityMonitoringScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AirQualityMonitoringScreen> createState() =>
|
|
||||||
_AirQualityMonitoringScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AirQualityMonitoringScreenState
|
|
||||||
extends State<AirQualityMonitoringScreen> {
|
|
||||||
String _selectedPeriod = "Hari Ini";
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Monitoring Kualitas Udara"),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Kualitas Udara",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
/// GRAFIK MONITORING
|
|
||||||
_buildMonitoringGraphBlock(),
|
|
||||||
const SizedBox(height: 25),
|
|
||||||
|
|
||||||
/// PENJELASAN
|
|
||||||
_buildAirQualityExplanation(),
|
|
||||||
const SizedBox(height: 25),
|
|
||||||
|
|
||||||
/// STATISTIK TAHUNAN
|
|
||||||
_buildAnnualAirQualityGraphSection(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// EXPORT FUNCTIONS
|
|
||||||
void _exportDailyAirQualityData() {
|
|
||||||
final StringBuffer csvContent = StringBuffer();
|
|
||||||
csvContent.writeln("DATA MONITORING KUALITAS UDARA - HARIAN (24 JAM)");
|
|
||||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Jam,AQI (Air Quality Index)");
|
|
||||||
|
|
||||||
final dailyData = [
|
|
||||||
68.0,
|
|
||||||
70.0,
|
|
||||||
72.0,
|
|
||||||
75.0,
|
|
||||||
78.0,
|
|
||||||
80.0,
|
|
||||||
78.0,
|
|
||||||
75.0,
|
|
||||||
72.0,
|
|
||||||
70.0,
|
|
||||||
68.0,
|
|
||||||
65.0,
|
|
||||||
60.0,
|
|
||||||
58.0,
|
|
||||||
60.0,
|
|
||||||
62.0,
|
|
||||||
65.0,
|
|
||||||
68.0,
|
|
||||||
70.0,
|
|
||||||
72.0,
|
|
||||||
75.0,
|
|
||||||
78.0,
|
|
||||||
76.0,
|
|
||||||
74.0
|
|
||||||
];
|
|
||||||
for (int i = 0; i < dailyData.length; i++) {
|
|
||||||
csvContent.writeln("$i:00,${dailyData[i]}");
|
|
||||||
}
|
|
||||||
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Statistik Harian");
|
|
||||||
csvContent.writeln("Total: 1709 AQI");
|
|
||||||
csvContent.writeln("Rata-rata: 71.2 AQI");
|
|
||||||
csvContent.writeln("Maksimal: 80 AQI");
|
|
||||||
csvContent.writeln("Minimal: 58 AQI");
|
|
||||||
|
|
||||||
showExportPreview(
|
|
||||||
context, csvContent.toString(), "Data Harian Kualitas Udara");
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exportWeeklyAirQualityData() {
|
|
||||||
final StringBuffer csvContent = StringBuffer();
|
|
||||||
csvContent.writeln("DATA MONITORING KUALITAS UDARA - MINGGUAN");
|
|
||||||
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Hari,Rata-rata AQI");
|
|
||||||
|
|
||||||
final days = [
|
|
||||||
'Senin',
|
|
||||||
'Selasa',
|
|
||||||
'Rabu',
|
|
||||||
'Kamis',
|
|
||||||
'Jumat',
|
|
||||||
'Sabtu',
|
|
||||||
'Minggu'
|
|
||||||
];
|
|
||||||
final weeklyData = [68.0, 72.0, 75.0, 78.0, 72.0, 68.0, 62.0];
|
|
||||||
|
|
||||||
for (int i = 0; i < days.length; i++) {
|
|
||||||
csvContent.writeln("${days[i]},${weeklyData[i]}");
|
|
||||||
}
|
|
||||||
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Statistik Mingguan");
|
|
||||||
csvContent.writeln("Total: 495 AQI");
|
|
||||||
csvContent.writeln("Rata-rata: 70.7 AQI");
|
|
||||||
csvContent.writeln("Maksimal: 78 AQI");
|
|
||||||
csvContent.writeln("Minimal: 62 AQI");
|
|
||||||
|
|
||||||
showExportPreview(
|
|
||||||
context, csvContent.toString(), "Data Mingguan Kualitas Udara");
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exportMonthlyAirQualityData() {
|
|
||||||
final StringBuffer csvContent = StringBuffer();
|
|
||||||
csvContent.writeln("DATA MONITORING KUALITAS UDARA - BULANAN");
|
|
||||||
csvContent.writeln(
|
|
||||||
"Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Tanggal,Rata-rata AQI");
|
|
||||||
|
|
||||||
for (int i = 1; i <= 28; i++) {
|
|
||||||
final value = (70.0 + (i % 6) * 2.5).toStringAsFixed(1);
|
|
||||||
csvContent.writeln("$i,$value");
|
|
||||||
}
|
|
||||||
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Statistik Bulanan");
|
|
||||||
csvContent.writeln("Total: 1990 AQI");
|
|
||||||
csvContent.writeln("Rata-rata: 71.1 AQI");
|
|
||||||
csvContent.writeln("Maksimal: 82 AQI");
|
|
||||||
csvContent.writeln("Minimal: 55 AQI");
|
|
||||||
|
|
||||||
showExportPreview(
|
|
||||||
context, csvContent.toString(), "Data Bulanan Kualitas Udara");
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exportAnnualAirQualityData() {
|
|
||||||
final StringBuffer csvContent = StringBuffer();
|
|
||||||
csvContent.writeln("DATA MONITORING KUALITAS UDARA - TAHUNAN");
|
|
||||||
csvContent.writeln("Tahun: 2025");
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("Bulan,Rata-rata AQI,AQI Max");
|
|
||||||
|
|
||||||
final months = [
|
|
||||||
'Januari',
|
|
||||||
'Februari',
|
|
||||||
'Maret',
|
|
||||||
'April',
|
|
||||||
'Mei',
|
|
||||||
'Juni',
|
|
||||||
'Juli',
|
|
||||||
'Agustus',
|
|
||||||
'September',
|
|
||||||
'Oktober',
|
|
||||||
'November',
|
|
||||||
'Desember'
|
|
||||||
];
|
|
||||||
final averages = [
|
|
||||||
68.0,
|
|
||||||
70.0,
|
|
||||||
72.5,
|
|
||||||
75.0,
|
|
||||||
78.0,
|
|
||||||
80.0,
|
|
||||||
82.0,
|
|
||||||
80.0,
|
|
||||||
75.0,
|
|
||||||
70.0,
|
|
||||||
68.0,
|
|
||||||
65.0
|
|
||||||
];
|
|
||||||
final maxAqi = [
|
|
||||||
85.0,
|
|
||||||
88.0,
|
|
||||||
90.0,
|
|
||||||
95.0,
|
|
||||||
98.0,
|
|
||||||
100.0,
|
|
||||||
102.0,
|
|
||||||
100.0,
|
|
||||||
95.0,
|
|
||||||
90.0,
|
|
||||||
85.0,
|
|
||||||
80.0
|
|
||||||
];
|
|
||||||
|
|
||||||
for (int i = 0; i < months.length; i++) {
|
|
||||||
csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}");
|
|
||||||
}
|
|
||||||
|
|
||||||
csvContent.writeln("");
|
|
||||||
csvContent.writeln("STATISTIK TAHUNAN");
|
|
||||||
csvContent.writeln("Rata-rata Tahunan: 74.2 AQI");
|
|
||||||
csvContent.writeln("AQI Max Teringgi: 102.0");
|
|
||||||
csvContent.writeln("AQI Min Terendah: 65.0");
|
|
||||||
|
|
||||||
showExportPreview(
|
|
||||||
context, csvContent.toString(), "Data Tahunan Kualitas Udara");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DATA GENERATORS
|
|
||||||
List<FlSpot> _getAirQualityPeriodData() {
|
|
||||||
switch (_selectedPeriod) {
|
|
||||||
case "Hari Ini":
|
|
||||||
return List.generate(24, (i) {
|
|
||||||
final values = [
|
|
||||||
68.0,
|
|
||||||
70.0,
|
|
||||||
72.0,
|
|
||||||
75.0,
|
|
||||||
78.0,
|
|
||||||
80.0,
|
|
||||||
78.0,
|
|
||||||
75.0,
|
|
||||||
72.0,
|
|
||||||
70.0,
|
|
||||||
68.0,
|
|
||||||
65.0,
|
|
||||||
60.0,
|
|
||||||
58.0,
|
|
||||||
60.0,
|
|
||||||
62.0,
|
|
||||||
65.0,
|
|
||||||
68.0,
|
|
||||||
70.0,
|
|
||||||
72.0,
|
|
||||||
75.0,
|
|
||||||
78.0,
|
|
||||||
76.0,
|
|
||||||
74.0
|
|
||||||
];
|
|
||||||
return FlSpot(i.toDouble(), values[i]);
|
|
||||||
});
|
|
||||||
case "Minggu Ini":
|
|
||||||
return [
|
|
||||||
const FlSpot(0, 68.0),
|
|
||||||
const FlSpot(1, 72.0),
|
|
||||||
const FlSpot(2, 75.0),
|
|
||||||
const FlSpot(3, 78.0),
|
|
||||||
const FlSpot(4, 72.0),
|
|
||||||
const FlSpot(5, 68.0),
|
|
||||||
const FlSpot(6, 62.0),
|
|
||||||
];
|
|
||||||
case "Bulan Ini":
|
|
||||||
return [
|
|
||||||
const FlSpot(0, 70.0),
|
|
||||||
const FlSpot(1, 72.5),
|
|
||||||
const FlSpot(2, 75.0),
|
|
||||||
const FlSpot(3, 77.5),
|
|
||||||
const FlSpot(4, 80.0),
|
|
||||||
const FlSpot(5, 82.0),
|
|
||||||
const FlSpot(6, 80.0),
|
|
||||||
const FlSpot(7, 78.0),
|
|
||||||
const FlSpot(8, 75.0),
|
|
||||||
const FlSpot(9, 72.0),
|
|
||||||
const FlSpot(10, 70.0),
|
|
||||||
const FlSpot(11, 68.0),
|
|
||||||
];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UI BUILDERS
|
|
||||||
Widget _buildMonitoringGraphBlock() {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Status Kualitas Udara",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 5,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.amber.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
"Normal",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.amber,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
/// PERIOD SELECTOR + EXPORT
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children:
|
|
||||||
["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: ChoiceChip(
|
|
||||||
label: Text(period),
|
|
||||||
selected: _selectedPeriod == period,
|
|
||||||
onSelected: (selected) {
|
|
||||||
setState(() {
|
|
||||||
_selectedPeriod = period;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectedColor: Colors.orange,
|
|
||||||
labelStyle: TextStyle(
|
|
||||||
color: _selectedPeriod == period
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
if (_selectedPeriod == "Hari Ini") {
|
|
||||||
_exportDailyAirQualityData();
|
|
||||||
} else if (_selectedPeriod == "Minggu Ini") {
|
|
||||||
_exportWeeklyAirQualityData();
|
|
||||||
} else if (_selectedPeriod == "Bulan Ini") {
|
|
||||||
_exportMonthlyAirQualityData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.download, size: 16),
|
|
||||||
label: const Text("Export"),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
textStyle: const TextStyle(fontSize: 11),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
/// GRAPH
|
|
||||||
SizedBox(
|
|
||||||
height: 300,
|
|
||||||
child: LineChart(
|
|
||||||
LineChartData(
|
|
||||||
gridData: FlGridData(
|
|
||||||
show: true,
|
|
||||||
drawHorizontalLine: true,
|
|
||||||
drawVerticalLine: false,
|
|
||||||
horizontalInterval: 1,
|
|
||||||
getDrawingHorizontalLine: (value) {
|
|
||||||
return FlLine(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
strokeWidth: 1,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
titlesData: FlTitlesData(
|
|
||||||
show: true,
|
|
||||||
bottomTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
interval: getPeriodInterval(_selectedPeriod),
|
|
||||||
getTitlesWidget: (value, meta) {
|
|
||||||
return Text(
|
|
||||||
getPeriodLabel(_selectedPeriod, value),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leftTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
interval: 1,
|
|
||||||
getTitlesWidget: (value, meta) {
|
|
||||||
return Text(
|
|
||||||
value.toInt().toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 9,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
reservedSize: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
topTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
rightTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
borderData: FlBorderData(
|
|
||||||
show: true,
|
|
||||||
border: Border(
|
|
||||||
left: BorderSide(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
lineBarsData: [
|
|
||||||
LineChartBarData(
|
|
||||||
spots: _getAirQualityPeriodData(),
|
|
||||||
isCurved: true,
|
|
||||||
barWidth: 2.5,
|
|
||||||
dotData: FlDotData(
|
|
||||||
show: true,
|
|
||||||
getDotPainter: (spot, percent, barData, index) {
|
|
||||||
return FlDotCirclePainter(
|
|
||||||
radius: 4,
|
|
||||||
color: Colors.orange,
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: Colors.orange,
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: Colors.orange.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAirQualityExplanation() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.orange.shade200),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Penjelasan Kualitas Udara",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.orange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Text(
|
|
||||||
"Kualitas udara diukur menggunakan Air Quality Index (AQI) yang menunjukkan tingkat polusi udara. AQI berkisar dari 0-500, dengan nilai lebih rendah menunjukkan udara lebih bersih. Monitoring kualitas udara penting untuk kesehatan masyarakat, perencanaan lingkungan, dan identifikasi sumber polusi. Kategori: 0-50 (Baik), 51-100 (Wajar), 101-150 (Tidak Sehat), 151+ (Sangat Tidak Sehat).",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
height: 1.6,
|
|
||||||
color: Colors.orange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnnualAirQualityGraphSection() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Statistik Tahunan Kualitas Udara",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
|
|
||||||
/// STATS
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "Rata-rata Tahunan",
|
|
||||||
value: "74.2 AQI",
|
|
||||||
color: Colors.orange,
|
|
||||||
icon: Icons.cloud,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
title: "AQI Maksimal",
|
|
||||||
value: "102.0",
|
|
||||||
color: Colors.red,
|
|
||||||
icon: Icons.warning,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
/// CHART
|
|
||||||
const Text(
|
|
||||||
"Grafik Tahunan - Rata-rata & AQI Maksimal",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Card(
|
|
||||||
elevation: 3,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 320,
|
|
||||||
child: LineChart(
|
|
||||||
LineChartData(
|
|
||||||
gridData: FlGridData(
|
|
||||||
show: true,
|
|
||||||
drawHorizontalLine: true,
|
|
||||||
drawVerticalLine: false,
|
|
||||||
horizontalInterval: 5,
|
|
||||||
getDrawingHorizontalLine: (value) {
|
|
||||||
return FlLine(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
strokeWidth: 1,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
titlesData: FlTitlesData(
|
|
||||||
show: true,
|
|
||||||
bottomTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
getTitlesWidget: (value, meta) {
|
|
||||||
final months = [
|
|
||||||
'Jan',
|
|
||||||
'Feb',
|
|
||||||
'Mar',
|
|
||||||
'Apr',
|
|
||||||
'Mei',
|
|
||||||
'Jun',
|
|
||||||
'Jul',
|
|
||||||
'Agu',
|
|
||||||
'Sep',
|
|
||||||
'Okt',
|
|
||||||
'Nov',
|
|
||||||
'Des'
|
|
||||||
];
|
|
||||||
return Text(
|
|
||||||
months[value.toInt()],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
reservedSize: 30,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leftTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(
|
|
||||||
showTitles: true,
|
|
||||||
getTitlesWidget: (value, meta) {
|
|
||||||
return Text(
|
|
||||||
'${value.toInt()} AQI',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 9,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
reservedSize: 50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
rightTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
topTitles: AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
borderData: FlBorderData(
|
|
||||||
show: true,
|
|
||||||
border: Border(
|
|
||||||
left: BorderSide(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
lineBarsData: [
|
|
||||||
/// Rata-rata
|
|
||||||
LineChartBarData(
|
|
||||||
spots: const [
|
|
||||||
FlSpot(0, 68.0),
|
|
||||||
FlSpot(1, 70.0),
|
|
||||||
FlSpot(2, 72.5),
|
|
||||||
FlSpot(3, 75.0),
|
|
||||||
FlSpot(4, 78.0),
|
|
||||||
FlSpot(5, 80.0),
|
|
||||||
FlSpot(6, 82.0),
|
|
||||||
FlSpot(7, 80.0),
|
|
||||||
FlSpot(8, 75.0),
|
|
||||||
FlSpot(9, 70.0),
|
|
||||||
FlSpot(10, 68.0),
|
|
||||||
FlSpot(11, 65.0),
|
|
||||||
],
|
|
||||||
isCurved: true,
|
|
||||||
barWidth: 2.5,
|
|
||||||
dotData: FlDotData(
|
|
||||||
show: true,
|
|
||||||
getDotPainter: (spot, percent, barData, index) {
|
|
||||||
return FlDotCirclePainter(
|
|
||||||
radius: 4,
|
|
||||||
color: Colors.orange,
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: Colors.orange,
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: Colors.orange.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// AQI Max
|
|
||||||
LineChartBarData(
|
|
||||||
spots: const [
|
|
||||||
FlSpot(0, 85.0),
|
|
||||||
FlSpot(1, 88.0),
|
|
||||||
FlSpot(2, 90.0),
|
|
||||||
FlSpot(3, 95.0),
|
|
||||||
FlSpot(4, 98.0),
|
|
||||||
FlSpot(5, 100.0),
|
|
||||||
FlSpot(6, 102.0),
|
|
||||||
FlSpot(7, 100.0),
|
|
||||||
FlSpot(8, 95.0),
|
|
||||||
FlSpot(9, 90.0),
|
|
||||||
FlSpot(10, 85.0),
|
|
||||||
FlSpot(11, 80.0),
|
|
||||||
],
|
|
||||||
isCurved: true,
|
|
||||||
barWidth: 2.5,
|
|
||||||
dotData: FlDotData(
|
|
||||||
show: true,
|
|
||||||
getDotPainter: (spot, percent, barData, index) {
|
|
||||||
return FlDotCirclePainter(
|
|
||||||
radius: 4,
|
|
||||||
color: Colors.red,
|
|
||||||
strokeWidth: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
|
|
||||||
/// LEGEND
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text(
|
|
||||||
"Rata-rata AQI",
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text(
|
|
||||||
"AQI Maksimal",
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
Center(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => _exportAnnualAirQualityData(),
|
|
||||||
icon: const Icon(Icons.file_download),
|
|
||||||
label: const Text("Download Excel Tahunan"),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
|
||||||
|
part 'atmospheric_conditions_event.dart';
|
||||||
|
part 'atmospheric_conditions_state.dart';
|
||||||
|
|
||||||
|
class AtmosphericConditionsBloc
|
||||||
|
extends Bloc<AtmosphericConditionsEvent, AtmosphericConditionsState> {
|
||||||
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription<AtmosphericConditions>? _subscription;
|
||||||
|
|
||||||
|
AtmosphericConditionsBloc({required MonitoringRepository repository})
|
||||||
|
: _repository = repository,
|
||||||
|
super(const AtmosphericConditionsState()) {
|
||||||
|
on<WatchAtmosphericConditionsStarted>(_onStarted);
|
||||||
|
on<_AtmosphericConditionsUpdated>(_onUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🚀 START
|
||||||
|
Future<void> _onStarted(
|
||||||
|
WatchAtmosphericConditionsStarted event,
|
||||||
|
Emitter<AtmosphericConditionsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
|
await _subscription?.cancel();
|
||||||
|
|
||||||
|
_subscription = _repository
|
||||||
|
.getSensorStream(
|
||||||
|
'sensor/latest',
|
||||||
|
(json) => AtmosphericConditions.fromJson(json),
|
||||||
|
)
|
||||||
|
.listen((data) {
|
||||||
|
add(_AtmosphericConditionsUpdated(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ⚡ REALTIME UPDATE
|
||||||
|
void _onUpdated(
|
||||||
|
_AtmosphericConditionsUpdated event,
|
||||||
|
Emitter<AtmosphericConditionsState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
temperature: event.data.temperature,
|
||||||
|
humidity: event.data.humidity,
|
||||||
|
pressure: event.data.pressure,
|
||||||
|
altitude: event.data.altitude,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// INTERNAL EVENT
|
||||||
|
class _AtmosphericConditionsUpdated extends AtmosphericConditionsEvent {
|
||||||
|
final AtmosphericConditions data;
|
||||||
|
|
||||||
|
const _AtmosphericConditionsUpdated(this.data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [data];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
part of 'atmospheric_conditions_bloc.dart';
|
||||||
|
|
||||||
|
abstract class AtmosphericConditionsEvent extends Equatable {
|
||||||
|
const AtmosphericConditionsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class WatchAtmosphericConditionsStarted extends AtmosphericConditionsEvent {}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
part of 'atmospheric_conditions_bloc.dart';
|
||||||
|
|
||||||
|
class AtmosphericConditionsState extends Equatable {
|
||||||
|
final double temperature;
|
||||||
|
final double humidity;
|
||||||
|
final double pressure;
|
||||||
|
final double altitude;
|
||||||
|
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const AtmosphericConditionsState({
|
||||||
|
this.temperature = 0.0,
|
||||||
|
this.humidity = 0.0,
|
||||||
|
this.pressure = 0.0,
|
||||||
|
this.altitude = 0.0,
|
||||||
|
this.isLoading = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
AtmosphericConditionsState copyWith({
|
||||||
|
double? temperature,
|
||||||
|
double? humidity,
|
||||||
|
double? pressure,
|
||||||
|
double? altitude,
|
||||||
|
bool? isLoading,
|
||||||
|
}) {
|
||||||
|
return AtmosphericConditionsState(
|
||||||
|
temperature: temperature ?? this.temperature,
|
||||||
|
humidity: humidity ?? this.humidity,
|
||||||
|
pressure: pressure ?? this.pressure,
|
||||||
|
altitude: altitude ?? this.altitude,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
temperature,
|
||||||
|
humidity,
|
||||||
|
pressure,
|
||||||
|
altitude,
|
||||||
|
isLoading,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../blocs/atmospheric_conditions_bloc.dart';
|
||||||
|
import '../../shared/utils/pdf/pdf_export_service.dart';
|
||||||
|
import '../../shared/widgets/export_pdf_button.dart';
|
||||||
|
|
||||||
|
class AtmosphericScreen extends StatelessWidget {
|
||||||
|
const AtmosphericScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
"Kondisi Atmosfer",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
body: BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_mainTemperature(state),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
_gridInfo(state),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// ← Tombol Export PDF
|
||||||
|
ExportPdfButton(
|
||||||
|
onExport: () => PdfExportService.atmospheric(
|
||||||
|
temperature: state.temperature,
|
||||||
|
humidity: state.humidity,
|
||||||
|
pressure: state.pressure,
|
||||||
|
altitude: state.altitude,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
// atmospheric state belum punya history,
|
||||||
|
// kalau nanti ditambah tinggal isi historyData di sini
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🌡️ TEMPERATURE (HERO CARD)
|
||||||
|
/// =========================
|
||||||
|
Widget _mainTemperature(AtmosphericConditionsState state) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
const Color.fromARGB(255, 38, 255, 222),
|
||||||
|
const Color.fromARGB(255, 53, 132, 229)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color.fromARGB(255, 0, 191, 255).withOpacity(0.3),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.thermostat, color: Colors.white, size: 50),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
state.temperature.toStringAsFixed(1),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 70,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
"°C",
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📊 INFO GRID
|
||||||
|
/// =========================
|
||||||
|
Widget _gridInfo(AtmosphericConditionsState state) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
mainAxisSpacing: 15,
|
||||||
|
crossAxisSpacing: 15,
|
||||||
|
childAspectRatio: 1.6,
|
||||||
|
children: [
|
||||||
|
_infoCard(
|
||||||
|
"Kelembapan",
|
||||||
|
"${state.humidity.toStringAsFixed(1)} %",
|
||||||
|
Icons.water_drop,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
_infoCard(
|
||||||
|
"Tekanan",
|
||||||
|
"${state.pressure.toStringAsFixed(1)} hPa",
|
||||||
|
Icons.speed,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
_infoCard(
|
||||||
|
"Ketinggian",
|
||||||
|
"${state.altitude.toStringAsFixed(1)} m",
|
||||||
|
Icons.terrain,
|
||||||
|
Colors.brown,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _infoCard(String title, String value, IconData icon, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 30),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
|
||||||
|
import '../../../../core/utils/time_series_mapper.dart';
|
||||||
|
|
||||||
|
part 'evaporasi_event.dart';
|
||||||
|
part 'evaporasi_state.dart';
|
||||||
|
|
||||||
|
class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription<Evaporasi>? _subscription;
|
||||||
|
|
||||||
|
EvaporasiBloc({required MonitoringRepository repository})
|
||||||
|
: _repository = repository,
|
||||||
|
super(const EvaporasiState()) {
|
||||||
|
on<WatchEvaporasiStarted>(_onStarted);
|
||||||
|
on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
|
on<EvaporasiPeriodChanged>(_onPeriodChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🚀 START
|
||||||
|
/// =========================
|
||||||
|
Future<void> _onStarted(
|
||||||
|
WatchEvaporasiStarted event,
|
||||||
|
Emitter<EvaporasiState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
|
final history = await _repository.getSensorHistory(
|
||||||
|
'evaporasi/history',
|
||||||
|
(json) => Evaporasi.fromJson(json),
|
||||||
|
);
|
||||||
|
|
||||||
|
final dailyGraph = TimeSeriesMapper.toDaily(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.evaporasi, // ⚠️ sesuaikan nama field
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
history: history,
|
||||||
|
dailyValues: dailyGraph,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
await _subscription?.cancel();
|
||||||
|
|
||||||
|
_subscription = _repository
|
||||||
|
.getSensorStream(
|
||||||
|
'Monitoring',
|
||||||
|
(json) => Evaporasi.fromJson(json),
|
||||||
|
)
|
||||||
|
.listen((data) {
|
||||||
|
add(_EvaporasiRealtimeUpdated(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// ⚡ REALTIME
|
||||||
|
/// =========================
|
||||||
|
void _onRealtimeUpdated(
|
||||||
|
_EvaporasiRealtimeUpdated event,
|
||||||
|
Emitter<EvaporasiState> emit,
|
||||||
|
) {
|
||||||
|
final updated = List<double>.from(state.dailyValues);
|
||||||
|
|
||||||
|
final index = DateTime.now().hour;
|
||||||
|
|
||||||
|
if (index < updated.length) {
|
||||||
|
updated[index] = event.data.evaporasi; // ⚠️ sesuaikan field
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
currentValue: event.data.evaporasi,
|
||||||
|
temperature: event.data.suhu,
|
||||||
|
waterLevel: event.data.tinggiAir,
|
||||||
|
dailyValues: updated,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📊 PERIOD
|
||||||
|
/// =========================
|
||||||
|
Future<void> _onPeriodChanged(
|
||||||
|
EvaporasiPeriodChanged event,
|
||||||
|
Emitter<EvaporasiState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||||
|
|
||||||
|
final history = state.history;
|
||||||
|
|
||||||
|
List<double> updated;
|
||||||
|
|
||||||
|
if (event.period == "Minggu Ini") {
|
||||||
|
updated = TimeSeriesMapper.toWeekly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.evaporasi,
|
||||||
|
);
|
||||||
|
} else if (event.period == "Bulan Ini") {
|
||||||
|
updated = TimeSeriesMapper.toMonthly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.evaporasi,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updated = TimeSeriesMapper.toDaily(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.evaporasi,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
dailyValues: updated,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// INTERNAL EVENT
|
||||||
|
class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
|
||||||
|
final Evaporasi data;
|
||||||
|
|
||||||
|
const _EvaporasiRealtimeUpdated(this.data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [data];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
part of 'evaporasi_bloc.dart';
|
||||||
|
|
||||||
|
abstract class EvaporasiEvent extends Equatable {
|
||||||
|
const EvaporasiEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🚀 START MONITORING
|
||||||
|
class WatchEvaporasiStarted extends EvaporasiEvent {}
|
||||||
|
|
||||||
|
/// 📊 GANTI PERIODE (Harian / Mingguan / Bulanan)
|
||||||
|
class EvaporasiPeriodChanged extends EvaporasiEvent {
|
||||||
|
final String period;
|
||||||
|
|
||||||
|
const EvaporasiPeriodChanged(this.period);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [period];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
part of 'evaporasi_bloc.dart';
|
||||||
|
|
||||||
|
class EvaporasiState extends Equatable {
|
||||||
|
final double currentValue; // nilai evaporasi realtime
|
||||||
|
final double temperature; // suhu (opsional dari firebase)
|
||||||
|
final double waterLevel; // tinggi air
|
||||||
|
final String selectedPeriod;
|
||||||
|
|
||||||
|
final List<double> dailyValues; // untuk grafik
|
||||||
|
final List<Evaporasi> history;
|
||||||
|
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const EvaporasiState({
|
||||||
|
this.currentValue = 0.0,
|
||||||
|
this.temperature = 0.0,
|
||||||
|
this.waterLevel = 0.0,
|
||||||
|
this.selectedPeriod = "Hari Ini",
|
||||||
|
this.dailyValues = const [],
|
||||||
|
this.history = const [],
|
||||||
|
this.isLoading = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
EvaporasiState copyWith({
|
||||||
|
double? currentValue,
|
||||||
|
double? temperature,
|
||||||
|
double? waterLevel,
|
||||||
|
String? selectedPeriod,
|
||||||
|
List<double>? dailyValues,
|
||||||
|
List<Evaporasi>? history,
|
||||||
|
bool? isLoading,
|
||||||
|
}) {
|
||||||
|
return EvaporasiState(
|
||||||
|
currentValue: currentValue ?? this.currentValue,
|
||||||
|
temperature: temperature ?? this.temperature,
|
||||||
|
waterLevel: waterLevel ?? this.waterLevel,
|
||||||
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
|
dailyValues: dailyValues ?? this.dailyValues,
|
||||||
|
history: history ?? this.history,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [
|
||||||
|
currentValue,
|
||||||
|
temperature,
|
||||||
|
waterLevel,
|
||||||
|
selectedPeriod,
|
||||||
|
dailyValues,
|
||||||
|
history,
|
||||||
|
isLoading,
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,167 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../blocs/evaporasi_bloc.dart';
|
||||||
|
import '../../shared/utils/pdf/pdf_export_service.dart';
|
||||||
|
import '../../shared/widgets/export_pdf_button.dart';
|
||||||
|
|
||||||
|
class EvaporasiScreen extends StatelessWidget {
|
||||||
|
const EvaporasiScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
"Evaporasi",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
body: BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final historyMaps = state.history
|
||||||
|
.map((e) => {
|
||||||
|
'timestamp': e.timestamp,
|
||||||
|
'evaporasi': e.evaporasi,
|
||||||
|
'suhu': e.suhu,
|
||||||
|
'tinggiAir': e.tinggiAir,
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_mainCard(state),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
_infoRow(state),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
_trendSection(),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
ExportPdfButton(
|
||||||
|
onExport: () => PdfExportService.evaporasi(
|
||||||
|
evaporasi: state.currentValue,
|
||||||
|
suhu: state.temperature,
|
||||||
|
tinggiAir: state.waterLevel,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
historyData: historyMaps.isNotEmpty ? historyMaps : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🔥 MAIN CARD (EVAPORASI)
|
||||||
|
/// =========================
|
||||||
|
Widget _mainCard(EvaporasiState state) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blue.shade400, Colors.blue.shade800],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.water_drop, color: Colors.white, size: 45),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
state.currentValue.toStringAsFixed(1),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 70,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
"mm",
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📊 INFO KECIL (SUHU & AIR)
|
||||||
|
/// =========================
|
||||||
|
Widget _infoRow(EvaporasiState state) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_miniCard(
|
||||||
|
"Suhu",
|
||||||
|
"${state.temperature.toStringAsFixed(1)} °C",
|
||||||
|
Icons.thermostat,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
_miniCard(
|
||||||
|
"Tinggi Air",
|
||||||
|
"${state.waterLevel.toStringAsFixed(1)} cm",
|
||||||
|
Icons.water,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _miniCard(String title, String value, IconData icon, Color color) {
|
||||||
|
return Container(
|
||||||
|
width: 160,
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📈 TREND (SIMPLE PLACEHOLDER)
|
||||||
|
/// =========================
|
||||||
|
Widget _trendSection() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
"Grafik Evaporasi",
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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};
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -3,51 +3,186 @@ import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
|
import '../../../../core/utils/time_series_mapper.dart';
|
||||||
|
|
||||||
part 'wind_speed_event.dart';
|
part 'wind_speed_event.dart';
|
||||||
part 'wind_speed_state.dart';
|
part 'wind_speed_state.dart';
|
||||||
|
|
||||||
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
final MonitoringRepository _repository;
|
final MonitoringRepository _repository;
|
||||||
|
StreamSubscription<MyWindSpeed>? _subscription;
|
||||||
|
|
||||||
WindSpeedBloc({required MonitoringRepository repository})
|
WindSpeedBloc({required MonitoringRepository repository})
|
||||||
: _repository = repository,
|
: _repository = repository,
|
||||||
super(const WindSpeedState()) {
|
super(const WindSpeedState()) {
|
||||||
// Handler saat aplikasi minta mulai monitoring
|
/// 🔥 START MONITORING
|
||||||
on<WatchWindSpeedStarted>(
|
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
||||||
(event, emit) async {
|
|
||||||
emit(state.copyWith(isLoading: true));
|
|
||||||
|
|
||||||
await emit.forEach<MyWindSpeed>(
|
/// 🔥 REALTIME UPDATE
|
||||||
_repository.getSensorStream(
|
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)),
|
|
||||||
onData: (data) {
|
|
||||||
// Mengambil list lama dan menambah data baru untuk grafik
|
|
||||||
final updatedSpeeds = List<double>.from(state.dailySpeeds)
|
|
||||||
..add(data.speed);
|
|
||||||
// Batasi jumlah data di grafik (misal cuma simpan 20 data terakhir agar tidak berat)
|
|
||||||
if (updatedSpeeds.length > 20) {
|
|
||||||
updatedSpeeds.removeAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.copyWith(
|
/// 🔥 CHANGE PERIOD
|
||||||
currentSpeed: data.speed,
|
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
||||||
dailySpeeds: updatedSpeeds, // Update list grafik
|
}
|
||||||
isLoading: false,
|
|
||||||
);
|
/// =========================
|
||||||
},
|
/// 🚀 START
|
||||||
onError: (error, stackTrace) => state.copyWith(isLoading: false),
|
/// =========================
|
||||||
); // emit forEach
|
Future<void> _onStarted(
|
||||||
},
|
WatchWindSpeedStarted event,
|
||||||
transformer: restartable(),
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
|
/// 1. Ambil history SEKALI
|
||||||
|
final history = await _repository.getSensorHistory(
|
||||||
|
'anemometer/history',
|
||||||
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
);
|
);
|
||||||
|
|
||||||
on<WindSpeedPeriodChanged>((event, emit) {
|
/// 2. Mapping default (harian)
|
||||||
emit(state.copyWith(selectedPeriod: event.period));
|
final raw = TimeSeriesMapper.toDaily(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
history: history,
|
||||||
|
dailySpeeds: dailyGraph,
|
||||||
|
weeklySpeeds: weekly,
|
||||||
|
monthlySpeeds: monthly,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
/// 3. Start realtime stream (manual subscription)
|
||||||
|
await _subscription?.cancel();
|
||||||
|
|
||||||
|
_subscription = _repository
|
||||||
|
.getSensorStream(
|
||||||
|
'anemometer/realtime',
|
||||||
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
|
)
|
||||||
|
.listen((data) {
|
||||||
|
add(_WindSpeedRealtimeUpdated(data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// ⚡ REALTIME UPDATE
|
||||||
|
/// =========================
|
||||||
|
void _onRealtimeUpdated(
|
||||||
|
_WindSpeedRealtimeUpdated event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) {
|
||||||
|
final updated = List<double>.from(state.dailySpeeds);
|
||||||
|
|
||||||
|
final index = DateTime.now().hour;
|
||||||
|
double newValue = event.data.speed;
|
||||||
|
|
||||||
|
if (index < updated.length) {
|
||||||
|
final lastValue = updated[index];
|
||||||
|
|
||||||
|
/// 🔥 ANTI SPIKE + SMOOTHING
|
||||||
|
if (lastValue != 0) {
|
||||||
|
if ((newValue - lastValue).abs() > 15) {
|
||||||
|
newValue = lastValue; // buang spike
|
||||||
|
} else {
|
||||||
|
newValue = (lastValue + newValue) / 2; // smoothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated[index] = newValue.clamp(0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
currentSpeed: newValue,
|
||||||
|
dailySpeeds: updated,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 📊 CHANGE PERIOD
|
||||||
|
/// =========================
|
||||||
|
Future<void> _onPeriodChanged(
|
||||||
|
WindSpeedPeriodChanged event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(selectedPeriod: event.period));
|
||||||
|
|
||||||
|
final history = state.history;
|
||||||
|
|
||||||
|
List<double> raw;
|
||||||
|
|
||||||
|
if (event.period == "Minggu Ini") {
|
||||||
|
raw = TimeSeriesMapper.toWeekly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
);
|
||||||
|
} else if (event.period == "Bulan Ini") {
|
||||||
|
raw = TimeSeriesMapper.toMonthly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
raw = TimeSeriesMapper.toDaily(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 baru di-smooth
|
||||||
|
final updatedGraph = TimeSeriesMapper.smooth(raw);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
dailySpeeds: updatedGraph,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// 🔒 INTERNAL EVENT (PRIVATE)
|
||||||
|
/// =========================
|
||||||
|
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
||||||
|
final MyWindSpeed data;
|
||||||
|
|
||||||
|
const _WindSpeedRealtimeUpdated(this.data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [data];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,31 +3,43 @@ 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;
|
||||||
|
|
||||||
const WindSpeedState({
|
const WindSpeedState({
|
||||||
this.currentSpeed = 0.0,
|
this.currentSpeed = 0.0,
|
||||||
this.selectedPeriod = "Hari Ini",
|
this.selectedPeriod = "Hari Ini",
|
||||||
this.dailySpeeds = const [],
|
this.dailySpeeds = const [],
|
||||||
this.isLoading = true,
|
this.isLoading = true,
|
||||||
|
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,
|
||||||
}) {
|
}) {
|
||||||
return WindSpeedState(
|
return WindSpeedState(
|
||||||
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props =>
|
List<Object> get props =>
|
||||||
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
|
[currentSpeed, selectedPeriod, dailySpeeds, isLoading, history];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/// === Tombol Filter Jam/Hari/Minggu === ///
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../blocs/wind_speed_bloc.dart';
|
||||||
|
|
||||||
|
class PeriodSelector extends StatelessWidget {
|
||||||
|
const PeriodSelector({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Kita ambil state saat ini untuk tahu periode mana yang aktif
|
||||||
|
final selectedPeriod =
|
||||||
|
context.select((WindSpeedBloc bloc) => bloc.state.selectedPeriod);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildTab(context, "Hari Ini", selectedPeriod),
|
||||||
|
_buildTab(context, "Minggu Ini", selectedPeriod),
|
||||||
|
_buildTab(context, "Bulan Ini", selectedPeriod),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTab(BuildContext context, String label, String current) {
|
||||||
|
bool isActive = label == current;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// Kirim event ke Bloc saat tombol diklik
|
||||||
|
context.read<WindSpeedBloc>().add(WindSpeedPeriodChanged(label));
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive ? Colors.blue : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isActive ? Colors.white : Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/// === File ini khusus untuk urusan Grafik === ///
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
// import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class WindSpeedChartWidget extends StatelessWidget {
|
||||||
|
final List<double> dailySpeeds;
|
||||||
|
final String period; // menambahkan parameter periode
|
||||||
|
|
||||||
|
const WindSpeedChartWidget(
|
||||||
|
{super.key, required this.dailySpeeds, required this.period});
|
||||||
|
|
||||||
|
double _getMaxY() {
|
||||||
|
if (dailySpeeds.isEmpty) return 10;
|
||||||
|
|
||||||
|
final max = dailySpeeds.reduce((a, b) => a > b ? a : b);
|
||||||
|
|
||||||
|
return (max + 5).clamp(10, 100); // kasih padding
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<double> safeData = dailySpeeds.map((e) {
|
||||||
|
if (e.isNaN || e.isInfinite) return 0.0;
|
||||||
|
return e < 0 ? 0.0 : e;
|
||||||
|
}).toList();
|
||||||
|
return Container(
|
||||||
|
height: 270,
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 20, 20, 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: LineChart(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
LineChartData(
|
||||||
|
minY: 0,
|
||||||
|
|
||||||
|
maxY: _getMaxY(),
|
||||||
|
gridData: const FlGridData(show: false),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
|
||||||
|
// === PENGATURAN LABEL SUMBU X ===
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
show: true,
|
||||||
|
rightTitles:
|
||||||
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
topTitles:
|
||||||
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
leftTitles:
|
||||||
|
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 30,
|
||||||
|
interval:
|
||||||
|
_getInterval(), // Mengatur jarak label agar tidak tumpang tindih
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
return SideTitleWidget(
|
||||||
|
meta: meta,
|
||||||
|
space: 8,
|
||||||
|
child: Text(
|
||||||
|
_getBottomTitle(value),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey, fontSize: 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}))),
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
spots: safeData.asMap().entries.map((e) {
|
||||||
|
return FlSpot(e.key.toDouble(), e.value);
|
||||||
|
}).toList(),
|
||||||
|
isCurved: true,
|
||||||
|
curveSmoothness: 0.2,
|
||||||
|
preventCurveOverShooting: true,
|
||||||
|
color: Colors.blue.shade700,
|
||||||
|
barWidth: 3,
|
||||||
|
isStrokeCapRound: true,
|
||||||
|
dotData: FlDotData(
|
||||||
|
show: true,
|
||||||
|
getDotPainter: (spot, percent, bar, index) {
|
||||||
|
if (index == dailySpeeds.length - 1) {
|
||||||
|
return FlDotCirclePainter(
|
||||||
|
radius: 5,
|
||||||
|
color: Colors.red,
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeColor: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FlDotCirclePainter(radius: 0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.blue.withOpacity(0.35),
|
||||||
|
Colors.blue.withOpacity(0.1),
|
||||||
|
Colors.transparent
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shadow: Shadow(
|
||||||
|
color: Colors.blue.withOpacity(0.3),
|
||||||
|
blurRadius: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
handleBuiltInTouches: true,
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
getTooltipItems: (touchedSpots) {
|
||||||
|
return touchedSpots.map((spot) {
|
||||||
|
return LineTooltipItem(
|
||||||
|
"${spot.y.toStringAsFixed(1)} m/s",
|
||||||
|
const TextStyle(color: Colors.white),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logika untuk menentukan teks label di bawah
|
||||||
|
String _getBottomTitle(double value) {
|
||||||
|
int index = value.toInt();
|
||||||
|
if (index < 0 || index >= dailySpeeds.length) return '';
|
||||||
|
|
||||||
|
if (period == "Hari Ini") {
|
||||||
|
// Tampilkan jam genap saja (0, 2, 4...) agar tidak penuh
|
||||||
|
return index % 4 == 0 ? "${index.toString().padLeft(2, '0')}:00" : "";
|
||||||
|
} else if (period == "Minggu Ini") {
|
||||||
|
const days = ["Sen", "Sel", "Rab", "Kam", "Jum", "Sab", "Min"];
|
||||||
|
return days[index % 7];
|
||||||
|
} else {
|
||||||
|
// Bulanan: Tampilkan tanggal kelipatan 5
|
||||||
|
return (index + 1) % 5 == 0 ? "${index + 1}" : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getInterval() {
|
||||||
|
if (period == "Hari Ini") return 1; // Cek tiap jam
|
||||||
|
if (period == "Minggu Ini") return 1; // Cek tiap hari
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,98 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
// import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../blocs/wind_speed_bloc.dart';
|
import '../blocs/wind_speed_bloc.dart';
|
||||||
|
import './widgets/wind_speed_chart_widget.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});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Pastikan MonitoringRepository sudah diprovide di atas screen ini
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Wind Speed Monitoring")),
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
"Kecepatan Angin",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
|
||||||
|
/// === Body area, lokasi dan tata letak Widgets === ///
|
||||||
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
// Pilih data sesuai periode
|
||||||
padding: const EdgeInsets.all(16.0),
|
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(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Card Informasi Kecepatan Saat Ini
|
/// ==== 1. Tampilan Angka Utama kecepatan angin (Hero Widget look) ==== ///
|
||||||
_buildCurrentSpeedCard(state.currentSpeed),
|
_buildMainSpeedDisplay(state.currentSpeed),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
// Area Grafik
|
const Text("Tren Kecepatan",
|
||||||
const Text("Grafik Kecepatan Angin",
|
|
||||||
style:
|
style:
|
||||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 15),
|
||||||
|
const PeriodSelector(),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
SizedBox(
|
WindSpeedChartWidget(
|
||||||
height: 300,
|
dailySpeeds: data,
|
||||||
child: LineChart(
|
period: state.selectedPeriod,
|
||||||
_mainData(state.dailySpeeds),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// 3. Info Tambahan (Status/Periode)
|
||||||
|
_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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -45,39 +103,75 @@ class WindSpeedScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCurrentSpeedCard(double speed) {
|
Widget _buildMainSpeedDisplay(double speed) {
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 4,
|
width: double.infinity,
|
||||||
child: ListTile(
|
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||||
leading: const Icon(Icons.wind_power, color: Colors.blue, size: 40),
|
decoration: BoxDecoration(
|
||||||
title: const Text("Kecepatan Saat Ini"),
|
gradient: LinearGradient(
|
||||||
trailing: Text("${speed.toStringAsFixed(2)} m/s",
|
colors: [Colors.blue.shade400, Colors.blue.shade800],
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.3),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.air, color: Colors.white, size: 50),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${speed.toStringAsFixed(1)}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 80, fontWeight: FontWeight.bold, color: Colors.white),
|
||||||
|
),
|
||||||
|
const Text("m/s",
|
||||||
|
style: TextStyle(fontSize: 20, color: Colors.white70))
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LineChartData _mainData(List<double> speeds) {
|
Widget _buildDetailRow(String period) {
|
||||||
return LineChartData(
|
return Row(
|
||||||
gridData: const FlGridData(show: true),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
titlesData: const FlTitlesData(show: true), // Bisa dikustom nanti
|
children: [
|
||||||
borderData: FlBorderData(show: true),
|
_miniInfoCard("Status", "Normal", Icons.check_circle, Colors.green),
|
||||||
lineBarsData: [
|
_miniInfoCard("Periode", period, Icons.timer, Colors.orange),
|
||||||
LineChartBarData(
|
|
||||||
// Mengubah List<double> menjadi titik koordinat grafik (x, y)
|
|
||||||
spots: speeds.asMap().entries.map((e) {
|
|
||||||
return FlSpot(e.key.toDouble(), e.value);
|
|
||||||
}).toList(),
|
|
||||||
isCurved: true, // Biar garisnya melengkung halus
|
|
||||||
color: Colors.blue,
|
|
||||||
barWidth: 3,
|
|
||||||
dotData: const FlDotData(show: false), // Sembunyikan titik per data
|
|
||||||
belowBarData: BarAreaData(
|
|
||||||
show: true,
|
|
||||||
color: Colors.blue.withOpacity(0.2), // Efek bayangan di bawah garis
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _miniInfoCard(String label, String value, IconData icon, Color color) {
|
||||||
|
return Container(
|
||||||
|
width: 160,
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 30),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||||
|
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,28 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
|
||||||
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
|
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
|
||||||
return mapper(data);
|
return mapper(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<T>> getSensorHistory<T>(
|
||||||
|
String path,
|
||||||
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Ambil data dari path history
|
||||||
|
final snapshot = await _db.ref(path).get();
|
||||||
|
|
||||||
|
if (snapshot.exists && snapshot.value is Map) {
|
||||||
|
final Map<dynamic, dynamic> data = snapshot.value as Map;
|
||||||
|
|
||||||
|
// Karena RTDB push() menghasilkan Map dengan ID unik,
|
||||||
|
// kita ambil values-nya saja dan ubah jadi List objek
|
||||||
|
return data.values.map((item) {
|
||||||
|
return mapper(item as Map<dynamic, dynamic>);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
class AtmosphericConditions {
|
||||||
|
final double temperature;
|
||||||
|
final double humidity;
|
||||||
|
final double pressure;
|
||||||
|
final double altitude;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
AtmosphericConditions({
|
||||||
|
required this.temperature,
|
||||||
|
required this.humidity,
|
||||||
|
required this.pressure,
|
||||||
|
required this.altitude,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AtmosphericConditions.fromJson(Map<dynamic, dynamic> json) {
|
||||||
|
return AtmosphericConditions(
|
||||||
|
temperature: (json['temperature'] ?? 0).toDouble(),
|
||||||
|
humidity: (json['humidity'] ?? 0).toDouble(),
|
||||||
|
pressure: (json['pressure'] ?? 0).toDouble(),
|
||||||
|
altitude: (json['altitude'] ?? 0).toDouble(),
|
||||||
|
timestamp: DateTime.now(), // karena kamu pakai latest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
class Evaporasi {
|
||||||
|
final double evaporasi;
|
||||||
|
final double suhu;
|
||||||
|
final double tinggiAir;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
Evaporasi({
|
||||||
|
required this.evaporasi,
|
||||||
|
required this.suhu,
|
||||||
|
required this.tinggiAir,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
static final empty = Evaporasi(
|
||||||
|
evaporasi: 0.0,
|
||||||
|
suhu: 0.0,
|
||||||
|
tinggiAir: 0.0,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
DateTime timestamp = now;
|
||||||
|
|
||||||
|
final waktuStr = json['waktu'] as String?;
|
||||||
|
if (waktuStr != null) {
|
||||||
|
final parts = waktuStr.split(':');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
final jam = int.tryParse(parts[0]) ?? 0;
|
||||||
|
final menit = int.tryParse(parts[1]) ?? 0;
|
||||||
|
final detik = parts.length >= 3 ? (int.tryParse(parts[2]) ?? 0) : 0;
|
||||||
|
timestamp = DateTime(now.year, now.month, now.day, jam, menit, detik);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Evaporasi(
|
||||||
|
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
||||||
|
suhu: (json['suhu_air'] ?? 0).toDouble(),
|
||||||
|
tinggiAir: (json['tinggi_air'] ?? 0).toDouble(),
|
||||||
|
|
||||||
|
/// 🔥 bikin timestamp dari jam & menit
|
||||||
|
timestamp: timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export 'wind_speed.dart';
|
export 'wind_speed.dart';
|
||||||
export 'evaporasi.dart';
|
export 'evaporasi.dart';
|
||||||
export 'pressure_sensor.dart';
|
export 'atmospheric_conditions.dart';
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,18 @@ class MyWindSpeed {
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static final empty = MyWindSpeed(
|
||||||
|
speed: 0.0,
|
||||||
|
pulse: 0,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
);
|
||||||
|
|
||||||
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
||||||
return MyWindSpeed(
|
return MyWindSpeed(
|
||||||
speed: (json['speed'] ?? 0).toDouble(),
|
speed: (json['speed'] ?? 0).toDouble(),
|
||||||
pulse: (json['pulse'] ?? 0) as int,
|
pulse: (json['pulse'] ?? 0) as int,
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
(json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik
|
(json['timesTamp'] ?? 0) * 1000, // kalau dari Unix detik
|
||||||
).toLocal(),
|
).toLocal(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
import 'models/models.dart';
|
// import 'models/models.dart';
|
||||||
|
|
||||||
abstract class MonitoringRepository {
|
abstract class MonitoringRepository {
|
||||||
|
// fungsi untuk ambil data berkali/streaming
|
||||||
Stream<T> getSensorStream<T>(
|
Stream<T> getSensorStream<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// fungsi untuk ambil sensor sekali
|
||||||
Future<T> getSensorSnapshot<T>(
|
Future<T> getSensorSnapshot<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fungsi untuk ambil riwayat (List)
|
||||||
|
Future<List<T>> getSensorHistory<T>(
|
||||||
|
String path,
|
||||||
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:user_repository/user_repository.dart';
|
import 'package:user_repository/user_repository.dart';
|
||||||
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
class FirebaseUserRepo implements UserRepository {
|
class FirebaseUserRepo implements UserRepository {
|
||||||
final FirebaseAuth _firebaseAuth;
|
final FirebaseAuth _firebaseAuth;
|
||||||
|
|
@ -72,6 +74,8 @@ class FirebaseUserRepo implements UserRepository {
|
||||||
/// == Melakukan Implement Log Out == ///
|
/// == Melakukan Implement Log Out == ///
|
||||||
@override
|
@override
|
||||||
Future<void> logOut() async {
|
Future<void> logOut() async {
|
||||||
|
final GoogleSignIn googleSignIn = GoogleSignIn();
|
||||||
|
await googleSignIn.signOut();
|
||||||
await _firebaseAuth.signOut();
|
await _firebaseAuth.signOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,26 +97,25 @@ class FirebaseUserRepo implements UserRepository {
|
||||||
@override
|
@override
|
||||||
Future<void> signInWithGoogle() async {
|
Future<void> signInWithGoogle() async {
|
||||||
try {
|
try {
|
||||||
final GoogleAuthProvider googleProvider = GoogleAuthProvider();
|
if (kIsWeb) {
|
||||||
final UserCredential userCredential =
|
final googleProvider = GoogleAuthProvider();
|
||||||
await _firebaseAuth.signInWithPopup(googleProvider);
|
await _firebaseAuth.signInWithPopup(googleProvider);
|
||||||
|
} else {
|
||||||
|
final GoogleSignIn googleSignIn = GoogleSignIn();
|
||||||
|
final googleUser = await googleSignIn.signIn();
|
||||||
|
|
||||||
final User? firebaseUser = userCredential.user;
|
if (googleUser == null) {
|
||||||
if (firebaseUser != null) {
|
throw Exception("cancelled");
|
||||||
// Check if user data already exists in Firestore
|
|
||||||
final existingUser = await userCollection.doc(firebaseUser.uid).get();
|
|
||||||
|
|
||||||
if (!existingUser.exists) {
|
|
||||||
// Create new user data from Google account info
|
|
||||||
final newUser = MyUser(
|
|
||||||
userId: firebaseUser.uid,
|
|
||||||
email: firebaseUser.email ?? '',
|
|
||||||
name: firebaseUser.displayName ?? 'User',
|
|
||||||
hasActiveCart: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setUserData(newUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GoogleSignInAuthentication googleAuth =
|
||||||
|
await googleUser.authentication;
|
||||||
|
|
||||||
|
final credential = GoogleAuthProvider.credential(
|
||||||
|
accessToken: googleAuth.accessToken,
|
||||||
|
idToken: googleAuth.idToken,
|
||||||
|
);
|
||||||
|
await _firebaseAuth.signInWithCredential(credential);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('Google sign-in error: $e');
|
log('Google sign-in error: $e');
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,54 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
google_identity_services_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_identity_services_web
|
||||||
|
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+1"
|
||||||
|
google_sign_in:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_sign_in
|
||||||
|
sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.0"
|
||||||
|
google_sign_in_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_android
|
||||||
|
sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.1"
|
||||||
|
google_sign_in_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_ios
|
||||||
|
sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.9.0"
|
||||||
|
google_sign_in_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_platform_interface
|
||||||
|
sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
|
google_sign_in_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_web
|
||||||
|
sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.4+4"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -335,4 +383,4 @@ packages:
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.27.0"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
google_sign_in: ^6.3.0
|
||||||
# equatable: ^2.0.5
|
# equatable: ^2.0.5
|
||||||
firebase_auth: ^6.1.3
|
firebase_auth: ^6.1.3
|
||||||
cloud_firestore: ^6.1.1
|
cloud_firestore: ^6.1.1
|
||||||
|
|
@ -23,4 +23,4 @@ dev_dependencies:
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^3.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
|
||||||
116
pubspec.lock
116
pubspec.lock
|
|
@ -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:
|
||||||
|
|
@ -292,42 +316,42 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_sign_in
|
name: google_sign_in
|
||||||
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
|
sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.0"
|
version: "6.3.0"
|
||||||
google_sign_in_android:
|
google_sign_in_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_sign_in_android
|
name: google_sign_in_android
|
||||||
sha256: "5ec98ab35387c68c0050495bb211bd88375873723a80fae7c2e9266ea0bdd8bb"
|
sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.7"
|
version: "6.2.1"
|
||||||
google_sign_in_ios:
|
google_sign_in_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_sign_in_ios
|
name: google_sign_in_ios
|
||||||
sha256: "234fc2830b55d1bbeb7e05662967691f5994143ff43dc70d3f139d1bbb3b8fb2"
|
sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.5"
|
version: "5.9.0"
|
||||||
google_sign_in_platform_interface:
|
google_sign_in_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_sign_in_platform_interface
|
name: google_sign_in_platform_interface
|
||||||
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
|
sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "2.5.0"
|
||||||
google_sign_in_web:
|
google_sign_in_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_sign_in_web
|
name: google_sign_in_web
|
||||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "0.12.4+4"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,15 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.1.0
|
flutter_bloc: ^8.1.0
|
||||||
google_fonts: ^8.0.2
|
google_fonts: ^8.0.2
|
||||||
google_sign_in: ^7.2.0
|
google_sign_in: ^6.3.0
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
monitoring_repository:
|
monitoring_repository:
|
||||||
path: packages/monitoring_repository
|
path: packages/monitoring_repository
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue